1. 命令行日常系快捷键

如下的快捷方式非常有用,能够极大的提升你的工作效率:
  • CTRL + U - 剪切光标前的内容
  • CTRL + K - 剪切光标至行末的内容
  • CTRL + Y - 粘贴
  • CTRL + E - 移动光标到行末
  • CTRL + A - 移动光标到行首
  • ALT + F - 跳向下一个空格
  • ALT + B - 跳回上一个空格
  • ALT + Backspace - 删除前一个单词
  • CTRL + W - 剪切光标后一个单词
  • Shift + Insert - 向终端内粘贴文本

2. 暂停并在后台运行命令

我曾经写过一篇如何在终端后台运行命令的指南。
  • CTRL + Z - 暂停应用程序
  • fg - 重新将程序唤到前台
如何使用这个技巧呢?
试想你正用nano编辑一个文件:
  1. sudo nano abc.txt
文件编辑到一半你意识到你需要马上在终端输入些命令,但是nano在前台运行让你不能输入。
你可能觉得唯一的方法就是保存文件,退出 nano,运行命令以后在重新打开nano。
其实你只要按CTRL + Z,前台的命令就会暂停,画面就切回到命令行了。然后你就能运行你想要运行命令,等命令运行完后在终端窗口输入“fg”就可以回到先前暂停的任务。
有一个尝试非常有趣就是用nano打开文件,输入一些东西然后暂停会话。再用nano打开另一个文件,输入一些什么后再暂停会话。如果你输入“fg”你将回到第二个用nano打开的文件。只有退出nano再输入“fg”,你才会回到第一个用nano打开的文件。

3. 在特定的时间运行Linux命令

使用命令‘at’
  1. at 10:38 PM Fri
  2. at> cowsay 'hello'
  3. at> CTRL + D
上面的命令能在周五下午10时38分运行程序cowsay。
使用的语法就是‘at’后追加日期时间。当at>提示符出现后就可以输入你想在那个时间运行的命令了。
CTRL + D 返回终端。
还有许多日期和时间的格式可以设置


惊声尖叫1996
睁开你的眼
钢琴师
基督受难记
无耻混蛋
纽约大逃亡
疯狂的麦克斯2
回到未来
全面回忆新
天兆
代码46
命运之门
8mm
沉默的羔羊
沉默的羔羊2
从黄昏到黎明12
开膛手

原文地址:https://www.ibm.com/developerworks/cn/java/the-new-features-of-Java-9/index.html
Java 9 正式发布于 2017 年 9 月 21 日 。作为 Java8 之后 3 年半才发布的新版本,Java 9 带 来了很多重大的变化。其中最重要的改动是 Java 平台模块系统的引入。除此之外,还有一些新的特性。 本文对 Java9 中包含的新特性做了概括性的介绍,可以帮助你快速了解 Java 9。

Java 平台 模块系统

Java 平台模块系统,也就是 Project Jigsaw,把模块化开发实践引入到了 Java 平台中。在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。这对于目前流行的不可变基础设施的实践来说,镜像的大小的减少可以节省很多存储空间和带宽资源 。
模块化开发的实践在软件开发领域并不是一个新的概念。Java 开发社区已经使用这样的模块化实践有相当长的一段时间。主流的构建工具,包括 Apache Maven 和 Gradle 都支持把一个大的项目划分成若干个子项目。子项目之间通过不同的依赖关系组织在一起。每个子项目在构建之后都会产生对应的 JAR 文件。 在 Java9 中 ,已有的这些项目可以很容易的升级转换为 Java 9 模块 ,并保持原有的组织结构不变。
Java 9 模块的重要特征是在其工件(artifact)的根目录中包含了一个描述模块的 module-info.class 文 件。 工件的格式可以是传统的 JAR 文件或是 Java 9 新增的 JMOD 文件。这个文件由根目录中的源代码文件 module-info.java 编译而来。该模块声明文件可以描述模块的不同特征。模块声明文件中可以包含的内容如下:
模块导出的包:使用 exports 可以声明模块对其他模块所导出的包。包中的 public 和 protected 类型,以及这些类型的 public 和 protected 成员可以被其他模块所访问。没有声明为导出的包相当于模块中的私有成员,不能被其他模块使用。
模块的依赖关系:使用 requires 可以声明模块对其他模块的依赖关系。使用 requires transitive 可 以把一个模块依赖声明为传递的。传递的模块依赖可以被依赖当前模块的其他模块所读取。 如果一个模块所导出的类型的型构中包含了来自它所依赖的模块的类型,那么对该模块的依赖应该声明为传递的。
服务的提供和使用:如果一个模块中包含了可以被 ServiceLocator 发现的服务接口的实现 ,需要使用 provides with 语句来声明具体的实现类 ;如果一个模块需要使用服务接口,可以使用 uses 语句来声明。
代码清单 1 中给出了一个模块声明文件的示例。在该声明文件中,模块 c om.mycompany.sample 导出了 Java 包 com.mycompany.sample。该模块依赖于模块 c om.mycompany.sample 。该模块也提供了服务接口 com.mycompany.common.DemoService 的实现类 c om.mycompany.sample.DemoServiceImpl 。
清单 1. 模块声明示例
1
2
3
4
5
6
module com.mycompany.sample {
    exports com.mycompany.sample;
    requires com.mycompany.common;
    provides com.mycompany.common.DemoService with
        com.mycompany.sample.DemoServiceImpl;
}
模块系统中增加了模块路径的概念。模块系统在解析模块时,会从模块路径中进行查找。为了保持与之前 Java 版本的兼容性,CLASSPATH 依然被保留。所有的类型在运行时都属于某个特定的模块。对于从 CLASSPATH 中加载的类型,它们属于加载它们的类加载器对应的未命名模块。可以通过 Class 的 getModule()方法来获取到表示其所在模块的 Module 对象。
在 JVM 启动时,会从应用的根模块开始,根据依赖关系递归的进行解析,直到得到一个表示依赖关系的图。如果解析过程中出现找不到模块的情况,或是在模块路径的同一个地方找到了名称相同的模块,模块解析过程会终止,JVM 也会退出。Java 也提供了相应的 API 与模块系统进行交互。

Jshell

jshell 是 Java 9 新增的一个实用工具。jshell 为 Java 增加了类似 NodeJS 和 Python 中的读取-求值-打印循环( Read-Evaluation-Print Loop ) 。 在 jshell 中 可以直接 输入表达式并查看其执行结果。当需要测试一个方法的运行效果,或是快速的对表达式进行求值时,jshell 都非常实用。只需要通过 jshell 命令启动 jshell,然后直接输入表达式即可。每个表达式的结果会被自动保存下来 ,以数字编号作为引用,类似 $1 和$2 这样的名称 。可以在后续的表达式中引用之前语句的运行结果。 在 jshell 中 ,除了表达式之外,还可以创建 Java 类和方法。jshell 也有基本的代码完成功能。
代码清单 2 中,我们直接创建了一个方法 add。
清单 2. 在 jshell 中添加方法
1
2
3
4
jshell> int add(int x, int y) {
    ...> return x + y;
    ...> }
 | created method add(int,int)
接着就可以在 jshell 中直接使用这个方法,如 代码清单 3 所示。
清单 3. 在 jshell 中使用创建的方法
1
2
jshell> add(1, 2)
$19 ==> 3

集合、Stream 和 Optional

在集合上,Java 9 增加 了 List.of()、Set.of()、Map.of() 和 M ap.ofEntries()等工厂方法来创建不可变集合 ,如 代码清单 4 所示。
清单 4 . 创建不可变集合
1
2
3
4
5
6
7
8
List.of();
List.of("Hello", "World");
List.of(1, 2, 3);
Set.of();
Set.of("Hello", "World");
Set.of(1, 2, 3);
Map.of();
Map.of("Hello", 1, "World", 2);
Stream 中增加了新的方法 ofNullable、dropWhile、takeWhile 和 iterate。在 代码清单 5 中,流中包含了从 1 到 5 的 元素。断言检查元素是否为奇数。第一个元素 1 被删除,结果流中包含 4 个元素。
清单 5 . Stream 中的 dropWhile 方法示例
1
2
3
4
5
6
7
@Test
public void testDropWhile() throws Exception {
    final long count = Stream.of(1, 2, 3, 4, 5)
        .dropWhile(i -> i % 2 != 0)
        .count();
    assertEquals(4, count);
}
Collectors 中增加了新的方法 filtering 和 flatMapping。在 代码清单 6 中,对于输入的 String 流 ,先通过 flatMapping 把 String 映射成 Integer 流 ,再把所有的 Integer 收集到一个集合中。
清单 6 . Collectors 的 flatMapping 方法示例
1
2
3
4
5
6
7
@Test
public void testFlatMapping() throws Exception {
    final Set<Integer> result = Stream.of("a", "ab", "abc")
        .collect(Collectors.flatMapping(v -> v.chars().boxed(),
            Collectors.toSet()));
    assertEquals(3, result.size());
}
Optiona l 类中新增了 ifPresentOrElse、or 和 stream 等方法。在 代码清单 7 中,Optiona l 流中包含 3 个 元素,其中只有 2 个有值。在使用 flatMap 之后,结果流中包含了 2 个值。
清单 7 . Optional 的 stream 方法示例
1
2
3
4
5
6
7
8
9
10
@Test
public void testStream() throws Exception {
    final long count = Stream.of(
        Optional.of(1),
        Optional.empty(),
        Optional.of(2)
    ).flatMap(Optional::stream)
        .count();
    assertEquals(2, count);
}

进程 API

Java 9 增加了 ProcessHandle 接口,可以对原生进程进行管理,尤其适合于管理长时间运行的进程。在使用 P rocessBuilder 来启动一个进程之后,可以通过 Process.toHandle()方法来得到一个 ProcessHandl e 对象的实例。通过 ProcessHandle 可以获取到由 ProcessHandle.Info 表 示的进程的基本信息,如命令行参数、可执行文件路径和启动时间等。ProcessHandle 的 onExit()方法返回一个 C ompletableFuture<ProcessHandle>对象,可以在进程结束时执行自定义的动作。 代码清单 8 中给出了进程 API 的使用示例。
清单 8 . 进程API 示例
1
2
3
4
5
6
7
8
9
10
final ProcessBuilder processBuilder = new ProcessBuilder("top")
    .inheritIO();
final ProcessHandle processHandle = processBuilder.start().toHandle();
processHandle.onExit().whenCompleteAsync((handle, throwable) -> {
    if (throwable == null) {
        System.out.println(handle.pid());
    } else {
        throwable.printStackTrace();
    }
});

平台日志 API 和 服务

Java 9 允许为 JDK 和应用配置同样的日志实现。新增的 System.LoggerFinder 用来管理 JDK 使 用的日志记录器实现。JVM 在运行时只有一个系统范围的 LoggerFinder 实例。LoggerFinder 通 过服务查找机制来加载日志记录器实现。默认情况下,JDK 使用 java.logging 模块中的 java.util.logging 实现。通过 LoggerFinder 的 getLogger()方法就可以获取到表示日志记录器的 System.Logger 实现。应用同样可以使用 System.Logger 来记录日志。这样就保证了 JDK 和应用使用同样的日志实现。我们也可以通过添加自己的 System.LoggerFinder 实现来让 JDK 和应用使用 SLF4J 等其他日志记录框架。 代码清单 9 中给出了平台日志 API 的使用示例。
清单 9.使用平台日志 API
1
2
3
4
5
6
public class Main {
    private static final System.Logger LOGGER = System.getLogger("Main");
    public static void main(final String[] args) {
        LOGGER.log(Level.INFO, "Run!");
    }
}

反应式流 ( Reactive Streams )

反应式编程的思想最近得到了广泛的流行。 在 Java 平台上有流行的反应式 库 RxJava 和 R eactor。反应式流规范的出发点是提供一个带非阻塞负压( non-blocking backpressure ) 的异步流处理规范。反应式流规范的核心接口已经添加到了 Java9 中的 java.util.concurrent.Flow 类中。
Flow 中包含了 Flow.Publisher、Flow.Subscriber、Flow.Subscription 和 F low.Processor 等 4 个核心接口。Java 9 还提供了 SubmissionPublisher 作为 Flow.Publisher 的一个实现。RxJava 2 和 Reactor 都可以很方便的 与 Flow 类的核心接口进行互操作。

变量句柄

变量句柄是一个变量或一组变量的引用,包括静态域,非静态域,数组元素和堆外数据结构中的组成部分等。变量句柄的含义类似于已有的方法句柄。变量句柄由 J ava 类 java.lang.invoke.VarHandle 来表示。可以使用类 j ava.lang.invoke.MethodHandles.Looku p 中的静态工厂方法来创建 VarHandle 对 象。通过变量句柄,可以在变量上进行各种操作。这些操作称为访问模式。不同的访问模式尤其在内存排序上的不同语义。目前一共有 31 种 访问模式,而每种访问模式都 在 VarHandle 中 有对应的方法。这些方法可以对变量进行读取、写入、原子更新、数值原子更新和比特位原子操作等。VarHandle 还 可以用来访问数组中的单个元素,以及把 byte[]数组 和 ByteBuffer 当成是不同原始类型的数组来访问。
 代码清单 10 中,我们创建了访问 HandleTarget 类中的域 count 的变量句柄,并在其上进行读取操作。
清单 10. 变量句柄使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HandleTarget {
    public int count = 1;
}
public class VarHandleTest {
    private HandleTarget handleTarget = new HandleTarget();
    private VarHandle varHandle;
    @Before
    public void setUp() throws Exception {
        this.handleTarget = new HandleTarget();
        this.varHandle = MethodHandles
            .lookup()
            .findVarHandle(HandleTarget.class, "count", int.class);
    }
    @Test
    public void testGet() throws Exception {
        assertEquals(1, this.varHandle.get(this.handleTarget));
        assertEquals(1, this.varHandle.getVolatile(this.handleTarget));
        assertEquals(1, this.varHandle.getOpaque(this.handleTarget));
        assertEquals(1, this.varHandle.getAcquire(this.handleTarget));
    }
}

改进方法句柄(Method Handle)

类 java.lang.invoke.MethodHandles 增加了更多的静态方法来创建不同类型的方法句柄。
arrayConstructor:创建指定类型的数组。
arrayLength:获取指定类型的数组的大小。
varHandleInvoker 和 varHandleExactInvoker:调用 VarHandle 中的访问模式方法。
zero:返回一个类型的默认值。
empty:返 回 MethodType 的返回值类型的默认值。
loop、countedLoop、iteratedLoop、whileLoop 和 doWhileLoop:创建不同类型的循环,包括 for 循环、while 循环 和 do-while 循环。
tryFinally:把对方法句柄的调用封装在 try-finally 语句中。
 代码清单 11 中,我们使用 iteratedLoop 来创建一个遍历 S tring 类型迭代器的方法句柄,并计算所有字符串的长度的总和。
清单 11. 循环方法句柄使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class IteratedLoopTest {
    static int body(final int sum, final String value) {
        return sum + value.length();
    }
    @Test
    public void testIteratedLoop() throws Throwable {
        final MethodHandle iterator = MethodHandles.constant(
            Iterator.class,
            List.of("a", "bc", "def").iterator());
        final MethodHandle init = MethodHandles.zero(int.class);
        final MethodHandle body = MethodHandles
            .lookup()
            .findStatic(
                IteratedLoopTest.class,
                "body",
                MethodType.methodType(
                    int.class,
                    int.class,
                    String.class));
        final MethodHandle iteratedLoop = MethodHandles
            .iteratedLoop(iterator, init, body);
        assertEquals(6, iteratedLoop.invoke());
    }
}

并发

在并发方面,类 CompletableFuture 中增加了几个新的方法。completeAsync 使用一个异步任务来获取结果并完成该 CompletableFuture。orTimeout 在 CompletableFuture 没有在给定的超时时间之前完成,使用 TimeoutException 异常来完成 CompletableFuture。completeOnTimeout 与 o rTimeout 类似,只不过它在超时时使用给定的值来完成 CompletableFuture。新的 Thread.onSpinWai t 方法在当前线程需要使用忙循环来等待时,可以提高等待的效率。

Nashorn

Nashorn 是 Java 8 中引入的新的 JavaScript 引擎。Java 9 中的 Nashorn 已经实现了一些 ECMAScript 6 规范中的新特性,包括模板字符串、二进制和八进制字面量、迭代器 和 for..of 循环和箭头函数等。Nashorn 还提供了 API 把 ECMAScript 源代码解析成抽象语法树( Abstract Syntax Tree,AST ) ,可以用来对 ECMAScript 源代码进行分析。

I/O 流新特性

类 java.io.InputStream 中增加了新的方法来读取和复制 InputStream 中包含的数据。
readAllBytes:读取 InputStream 中的所有剩余字节。
readNBytes: 从 InputStream 中读取指定数量的字节到数组中。
transferTo:读取 InputStream 中的全部字节并写入到指定的 OutputStream 中 。
代码清单 12 中给出了这些新方法的使用示例。
清单 12. InputStream 中的新方法使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TestInputStream {
    private InputStream inputStream;
    private static final String CONTENT = "Hello World";
    @Before
    public void setUp() throws Exception {
        this.inputStream =
            TestInputStream.class.getResourceAsStream("/input.txt");
    }
    @Test
    public void testReadAllBytes() throws Exception {
        final String content = new String(this.inputStream.readAllBytes());
        assertEquals(CONTENT, content);
    }
    @Test
    public void testReadNBytes() throws Exception {
        final byte[] data = new byte[5];
        this.inputStream.readNBytes(data, 0, 5);
        assertEquals("Hello", new String(data));
    }
    @Test
    public void testTransferTo() throws Exception {
        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        this.inputStream.transferTo(outputStream);
        assertEquals(CONTENT, outputStream.toString());
    }
}
ObjectInputFilter 可以对 ObjectInputStream 中 包含的内容进行检查,来确保其中包含的数据是合法的。可以使用 ObjectInputStream 的方法 setObjectInputFilter 来设置。ObjectInputFilter 在 进行检查时,可以检查如对象图的最大深度、对象引用的最大数量、输入流中的最大字节数和数组的最大长度等限制,也可以对包含的类的名称进行限制。

改进应用安全性能

Java 9 新增了 4 个 SHA- 3 哈希算法,SHA3-224、SHA3-256、SHA3-384 和 S HA3-512。另外也增加了通过 java.security.SecureRandom 生成使用 DRBG 算法的强随机数。 代码清单 13 中给出了 SHA-3 哈希算法的使用示例。

清单 13. SHA-3 哈希算法使用示例

清单 13. SHA-3 哈希算法使用示例
1
2
3
4
5
6
7
8
public class SHA3 {
    public static void main(final String[] args) throws NoSuchAlgorithmException {
        final MessageDigest instance = MessageDigest.getInstance("SHA3-224");
        final byte[] digest = instance.digest("".getBytes());
        System.out.println(Hex.encodeHexString(digest));
    }
}

用户界面

类 java.awt.Desktop 增加了新的与桌面进行互动的能力。可以使用 addAppEventListener 方法来添加不同应用事件的监听器,包括应用变为前台应用、应用隐藏或显示、屏幕和系统进入休眠与唤醒、以及 用户会话的开始和终止等。还可以在显示关于窗口和配置窗口时,添加自定义的逻辑。在用户要求退出应用时,可以通过自定义处理器来接受或拒绝退出请求。在 A WT 图像支持方面,可以在应用中使用多分辨率图像。

统一 JVM 日志

Java 9 中 ,JVM 有了统一的日志记录系统,可以使用新的命令行选项-Xlog 来控制 JVM 上 所有组件的日志记录。该日志记录系统可以设置输出的日志消息的标签、级别、修饰符和输出目标等。Java 9 移除了在 Java 8 中 被废弃的垃圾回收器配置组合,同时 把 G1 设为默认的垃圾回收器实现。另外,CMS 垃圾回收器已经被声明为废弃。Java 9 也增加了很多可以通过 jcmd 调用的诊断命令。

其他改动方面

在 Java 语言本身,Java 9 允许在接口中使用私有方法。 在 try-with-resources 语句中可以使用 e ffectively-final 变量。 类 java.lang.StackWalker 可 以对线程的堆栈进行遍历,并且支持过滤和延迟访问。Java 9 把对 Unicode 的支持升级到了 8.0。ResourceBundle 加载属性文件的默认编码从 ISO-8859-1 改成了 UTF-8,不再需要使用 native2ascii 命 令来对属性文件进行额外处理。注解@Deprecated 也得到了增强,增加了 since 和 forRemoval 两 个属性,可以分别指定一个程序元素被废弃的版本,以及是否会在今后的版本中被删除。
 代码清单 14 中,buildMessage 是接口 SayHi 中的私有方法,在默认方法 sayHi 中被使用。
清单 14. 接口中私有方法的示例
1
2
3
4
5
6
7
8
9
public interface SayHi {
    private String buildMessage() {
        return "Hello";
    }
    void sayHi(final String message);
    default void sayHi() {
        sayHi(buildMessage());
    }
}

小结

作为 Java 平台最新的一个重大更新,Java 9 中的很多新特性,尤其模块系统,对于 Java 应用的开发会产生深远的影响。本文对 Java 9 中的新特性做了概括的介绍,可以作为了解 Java 9 的基础。这些新特性的相信内容,可以通过官方文档来进一步的了解。

参考资源 (resources )

参考 Java 9 官方文档 ,了解 Java 9 的更多内容 。
参考 Java 9 官方 Java 文档 ,了解 Java API 的细节。了解 反应式流规范 的更多内容 。

源码地址:https://github.com/FanHuaRan/ran-stl/blob/master/alo/ran_math.cpp
#include "ran_math.h"
/**
 *基于位运算实现的加减乘除法
 *
 *
 */
//加法实现
int add(int num1, int num2) {
       int sum = num1 ^ num2;//不考虑进一的加法可以使用异或计算
       int carry = (num1 & num2) << 1;//进一的结果可以使用与运算符左移一位得到
       while (carry != 0){//当没有进一时加法结束
              //迭代
              int tempSum = sum ^ carry;
              carry = (sum & carry) << 1;
              sum = tempSum;
       }
       return sum;
}
//减法实现
int sub(int num1, int num2) {
       //首先取得num2的负数 负数表示是正数的补码
       int oppositor = add(~num2, 1);
       //于是num1+num2可以使用num1+(-num2)来表示
       return add(num1, oppositor);
}
//乘法实现
int multip(int num1, int num2) {
       //计算结果的绝对值
       int absNum1 = num1 < 0 ? add(~num1, 1) : num1;
       int absNum2 = num2 < 0 ? add(~num2, 1) : num2;
       int result = 0;
       /*
       for (int i = 0; i < absNum2; i++) {
              result += absNum1;
       }
       */
       int multipcand = absNum1;//被乘数
       int multiper = absNum2;//乘数
       while (multiper){
              if (multiper & 0x1) {//如果乘数的最后一位为1 则加上一个被乘数
                     result = add(result, multipcand);
              }
              multipcand <<= 1;//被乘数左移一位
              multiper >>= 1;//乘数右移一位
       }
       //判断符号
       if ((num1 ^ num2) < 0) {//异号返回负数
              return add(~result, 1);
       }
       else {
              return result;
       }
}
//除法实现
int division(int num1, int num2) {
       // 先取被除数和除数的绝对值   
       int dividend = num1 > 0 ? num1 : add(~num1, 1);
       int divisor = num2 > 0 ? num2 : add(~num2, 1);
       int quotient = 0;// 商   
       int remainder = 0;// 余数   
       for (int i = 31; i >= 0; i--) {
              //比较dividend是否大于divisor的(1<<i)次方,不要将dividend与(divisor<<i)比较,而是用(dividend>>i)与divisor比较,
              //效果一样,但是可以避免因(divisor<<i)操作可能导致的溢出,如果溢出则会可能dividend本身小于divisor,但是溢出导致dividend大于divisor      
              if ((dividend >> i) >= divisor) {
                     quotient = add(quotient, 1 << i);
                     dividend = sub(dividend, divisor << i);
              }
       }
       // 确定商的符号   
       if ((num1 ^ num2) < 0) {
              // 如果除数和被除数异号,则商为负数       
              quotient = add(~quotient, 1);
       }
       // 确定余数符号   
       remainder = num2 > 0 ? dividend : add(~dividend, 1);
       return quotient;// 返回商
}
原文
地址:https://www.jianshu.com/p/7bba031b11e7

位运算实现加、减、乘、除运算

我们知道,计算机最基本的操作单元是字节(byte),一个字节由8个位(bit)组成,一个位只能存储一个0或1,其实也就是高低电平。无论多么复杂的逻辑、庞大的数据、酷炫的界面,最终体现在计算机最底层都只是对0101的存储和运算。因此,了解位运算有助于提升我们对计算机底层操作原理的理解。
今天就来看看怎么不使用显式“ + - * /”运算符来实现加减乘除运算。
下面我们一个一个来看。

1. 加法运算

先来个我们最熟悉的十进制的加法运算:
13 + 9 = 22
我们像这样来拆分这个运算过程:
  1. 不考虑进位,分别对各位数进行相加,结果为sum:
    个位数3加上9为2;十位数1加上0为1; 最终结果为12;
  2. 只考虑进位,结果为carry:
    3 + 9 有进位,进位的值为10;
  3. 如果步骤2所得进位结果carry不为0,对步骤1所得sum,步骤2所得carry重复步骤1、 2、3;如果carry为0则结束,最终结果为步骤1所得sum:
    这里即是对sum = 12 和carry = 10重复以上三个步骤,(a) 不考虑进位,分别对各位数进行相加:sum = 22; (b) 只考虑进位: 上一步没有进位,所以carry = 0; (c) 步骤2carry = 0,结束,结果为sum = 22.
我们发现这三板斧行得通!
那我们现在还使用上面的三板斧把十进制运算放在二进制中看看是不是也行的通。
13的二进制为0000 1101,9的二进制为0000 1001:
  1. 不考虑进位,分别对各位数进行相加:
    sum = 0000 1101 + 0000 1001 = 0000 0100
  2. 考虑进位:
    有两处进位,第0位和第3位,只考虑进位的结果为:
    carry = 0001 0010
  3. 步骤2carry == 0 ?,不为0,重复步骤1 、2 、3;为0则结束,结果为sum:
    本例中,
    (a)不考虑进位sum = 0001 0110;
    (b)只考虑进位carry = 0;
    (c)carry == 0,结束,结果为sum = 0001 0110
    转换成十进制刚好是22.
我们发现,适用于十进制的三板斧同样适用于二进制!仔细观察者三板斧,大家能不能发现其实第一步不考虑进位的加法其实就是异或运算;而第二部只考虑进位就是与运算并左移一位--;第三步就是重复前面两步操作直到第二步进位结果为0。
这里关于第三步多说一点。为什么要循环步骤1、 2、 3直到步骤2所得进位carry等于0?其实这是因为有的数做加法时会出现连续进位的情况,举例:3 + 9,我们来走一遍上述逻辑:
a = 0011, b = 1001;
start;
first loop;
1.1 sum = 1010
1.2 carry = 0010
1.3 carry != 0 , go on;
second loop;
2.1 sum = 1000;
2.2 carry = 0100;
2.3 carry != 0, go on;
third loop;
3.1 sum = 1100;
3.2 carry = 0000;
3.3 carry == 0, stop; result = sum;
end
如上面的栗子,有的加法操作是有连续进位的情况的,所以这里要在第三步检测carry是不是为0,如果为0则表示没有进位了,第一步的sum即为最终的结果。
有了上面的分析,我们不难写出如下代码:
// 递归写法int add(int num1, int num2){
if(num2 == 0)
return num1;
int sum = num1 ^ num2;
int carry = (num1 & num2) << 1;
return add(sum, carry);
}

// 迭代写法int add(int num1, int num2){
int sum = num1 ^ num2;
int carry = (num1 & num2) << 1;
while(carry != 0){
int a = sum;
int b = carry;
sum = a ^ b;
carry = (a & b) << 1;
}
return sum;
}
我们的计算机其实就是通过上述的位运算实现加法运算的(通过加法器,加法器就是使用上述的方法实现加法的),而程序语言中的+ - * /运算符只不过是呈现给程序员的操作工具,计算机底层实际操作的永远是形如0101的位,所以说位运算真的很重要!

2. 减法运算

我们知道了位运算实现加法运算,那减法运算就相对简单一些了。我们实现了加法运算,自然的,我们会想到把减法运算11 - 6变形为加法运算11 + (-6),即一个正数加上一个负数。是的,很聪明,其实我们的计算机也是这样操作的,那有的人会说为什么计算机不也像加法器一样实现一个减法器呢?对的,这样想当然是合理的,但是考虑到减法比加法来的复杂,实现起来比较困难。为什么呢?我们知道加法运算其实只有两个操作,加、 进位,而减法呢,减法会有借位操作,如果当前位不够减那就从高位借位来做减法,这里就会问题了,借位怎么表示呢?加法运算中,进位通过与运算并左移一位实现,而借位就真的不好表示了。所以我们自然的想到将减法运算转变成加法运算。
怎么实现呢?
刚刚我们说了减法运算可转变成一个正数加上一个负数,那首先就要来看看负数在计算机中是怎么表示的。
+8在计算机中表示为二进制的1000,那-8怎么表示呢?
很容易想到,可以将一个二进制位(bit)专门规定为符号位,它等于0时就表示正数,等于1时就表示负数。比如,在8位机中,规定每个字节的最高位为符号位。那么,+8就是00001000,而-8则是10001000。这只是直观的表示方法,其实计算机是通过2的补码来表示负数的,那什么是2的补码(同补码,英文是2's complement,其实应该翻译为2的补码)呢?它是一种用二进制表示有号数的方法,也是一种将数字的正负号变号的方式,求取步骤:
  • 第一步,每一个二进制位都取相反值,0变成1,1变成0(即反码)。
  • 第二步,将上一步得到的值(反码)加1。
简单来说就是取反加一!
关于补码更详细的内容可参维基百科-补码,这里不再赘述。
其实我们利用的恰巧是补码的可以将数字的正负号变号的功能,这样我们就可以把减法运算转变成加法运算了,因为负数可以通过其对应正数求补码得到。计算机也是通过增加一个补码器配合加法器来做减法运算的,而不是再重新设计一个减法器。
以上,我们很容易写出了位运算做减法运算的代码:
/*
* num1: 减数
* num2: 被减数
*/int substract(int num1, int num2){
int subtractor = add(~num2, 1);// 先求减数的补码(取反加一)
int result = add(num1, subtractor); // add()即上述加法运算  
return result ;
}

3. 乘法运算

我们知道了加法运算的位运算实现,那很容易想到乘法运算可以转换成加法运算,被乘数加上乘数倍的自己不就行了么。这里还有一个问题,就是乘数和被乘数的正负号问题,我们这样处理,先处理乘数和被乘数的绝对值的乘积,然后根据它们的符号确定最终结果的符号即可。步骤如下:
(1) 计算绝对值得乘积
(2) 确定乘积符号(同号为证,异号为负)
有了这个思路,代码就不难写了:
/*
* a: 被乘数
* b: 乘数
*/int multiply(int a, int b){
// 取绝对值  
int multiplicand = a < 0 ? add(~a, 1) : a;
int multiplier = b < 0 ? add(~b , 1) : b;// 如果为负则取反加一得其补码,即正数  
// 计算绝对值的乘积  
int product = 0;
int count = 0;
while(count < multiplier) {
product = add(product, multiplicand);
count = add(count, 1);// 这里可别用count++,都说了这里是位运算实现加法  
}
// 确定乘积的符号  
if((a ^ b) < 0) {// 只考虑最高位,如果a,b异号,则异或后最高位为1;如果同号,则异或后最高位为0;    
product = add(~product, 1);
}
return product;
}
上面的思路在步骤上没有问题,但是第一步对绝对值作乘积运算我们是通过不断累加的方式来求乘积的,这在乘数比较小的情况下还是可以接受的,但在乘数比较大的时候,累加的次数也会增多,这样的效率不是最高的。我们可以思考,如何优化求绝对值的乘积这一步。
考虑我们现实生活中手动求乘积的过程,这种方式同样适用于二进制,下面我以13*14为例,向大家演示如何用手动计算的方式求乘数和被乘数绝对值的乘积。
从上图的计算过程可以看出,如果乘数当前位为1,则取被乘数左移一位的结果加到最终结果中;如果当前位为0,则取0加到乘积中(加0也就是什么也不做);
整理成算法步骤:
(1) 判断乘数是否为0,为0跳转至步骤(4)
(2) 将乘数与1作与运算,确定末尾位为1还是为0,如果为1,则相加数为当前被乘数;如果为0,则相加数为0;将相加数加到最终结果中;
(3) 被乘数左移一位,乘数右移一位;回到步骤(1)
(4) 确定符号位,输出结果;
代码如下:
int multiply(int a, int b) {  
//将乘数和被乘数都取绝对值 
int multiplicand = a < 0 ? add(~a, 1) : a;   
int multiplier = b < 0 ? add(~b , 1) : b;  
 
//计算绝对值的乘积  
int product = 0;  
while(multiplier > 0) {    
if((multiplier & 0x1) > 0) {// 每次考察乘数的最后一位    
product = add(product, multiplicand);    
}     
multiplicand = multiplicand << 1;// 每运算一次,被乘数要左移一位    
multiplier = multiplier >> 1;// 每运算一次,乘数要右移一位(可对照上图理解)  
}   
//计算乘积的符号  
if((a ^ b) < 0) {    
product = add(~product, 1);  
}   
return product;
}

显而易见,第二种求乘积的方式明显要优于第一种。

4. 除法运算

除法运算很容易想到可以转换成减法运算,即不停的用除数去减被除数,直到被除数小于除数时,此时所减的次数就是我们需要的商,而此时的被除数就是余数。这里需要注意的是符号的确定,商的符号和乘法运算中乘积的符号确定一样,即取决于除数和被除数,同号为证,异号为负;余数的符号和被除数一样。
代码如下:
/*
* a : 被除数
* b : 除数
*/int divide(int a, int b){
// 先取被除数和除数的绝对值
int dividend = a > 0 ? a : add(~a, 1);
int divisor = b > 0 ? a : add(~b, 1);

int quotient = 0;// 商
int remainder = 0;// 余数
// 不断用除数去减被除数,直到被除数小于被除数(即除不尽了)
while(dividend >= divisor){// 直到商小于被除数
quotient = add(quotient, 1);
dividend = substract(dividend, divisor);
}
// 确定商的符号
if((a ^ b) < 0){// 如果除数和被除数异号,则商为负数
quotient = add(~quotient, 1);
}
// 确定余数符号
remainder = b > 0 ? dividend : add(~dividend, 1);
return quotient;// 返回商
}
这里有和简单版乘法运算一样的问题,如果被除数非常大,除数非常小,那就要进行很多次减法运算,有没有更简便的方法呢?
上面的代码之所以比较慢是因为步长太小,每次只能用1倍的除数去减被除数,所以速度比较慢。那能不能增大步长呢?如果能,应该怎么增大步长呢?
计算机是一个二元的世界,所有的int型数据都可以用[2^0, 21,...,231]这样一组基来表示(int型最高31位)。不难想到用除数的231,230,...,22,21,2^0倍尝试去减被除数,如果减得动,则把相应的倍数加到商中;如果减不动,则依次尝试更小的倍数。这样就可以快速逼近最终的结果。
2的i次方其实就相当于左移i位,为什么从31位开始呢?因为int型数据最大值就是2^31啊。
代码如下:
int divide_v2(int a,int b) {
// 先取被除数和除数的绝对值
int dividend = a > 0 ? a : add(~a, 1);
int divisor = b > 0 ? a : add(~b, 1);
int quotient = 0;// 商
int remainder = 0;// 余数
for(int i = 31; i >= 0; i--) {
//比较dividend是否大于divisor的(1<<i)次方,不要将dividend与(divisor<<i)比较,而是用(dividend>>i)与divisor比较,
//效果一样,但是可以避免因(divisor<<i)操作可能导致的溢出,如果溢出则会可能dividend本身小于divisor,但是溢出导致dividend大于divisor
if((dividend >> i) >= divisor) {
quotient = add(quotient, 1 << i);
dividend = substract(dividend, divisor << i);
}
}
// 确定商的符号
if((a ^ b) < 0){
// 如果除数和被除数异号,则商为负数
quotient = add(~quotient, 1);
}
// 确定余数符号
remainder = b > 0 ? dividend : add(~dividend, 1);
return quotient;// 返回商
}
好了,以上。



原文地址:https://www.ibm.com/developerworks/cn/java/j-jtp11225/
用弱引用堵住内存泄漏
弱引用使得表达对象生命周期关系变得容易了
要让垃圾收集(GC)回收程序不再使用的对象,对象的逻辑 生命周期(应用程序使用它的时间)和对该对象拥有的引用的实际 生命周期必须是相同的。在大多数时候,好的软件工程技术保证这是自动实现的,不用我们对对象生命周期问题花费过多心思。但是偶尔我们会创建一个引用,它在内存中包含对象的时间比我们预期的要长得多,这种情况称为无意识的对象保留(unintentional object retention)

全局 Map 造成的内存泄漏

无意识对象保留最常见的原因是使用 Map 将元数据与临时对象(transient object)相关联。假定一个对象具有中等生命周期,比分配它的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字连接。需要将一些元数据与这个套接字关联,如生成连接的用户的标识。在创建 Socket 时是不知道这些信息的,并且不能将数据添加到 Socket 对象上,因为不能控制 Socket 类或者它的子类。这时,典型的方法就是在一个全局 Map 中存储这些信息,如清单 1 中的 SocketManager 类所示:
清单 1. 使用一个全局 Map 将元数据关联到一个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SocketManager {
    private Map<Socket,User> m = new HashMap<Socket,User>();
     
    public void setUser(Socket s, User u) {
        m.put(s, u);
    }
    public User getUser(Socket s) {
        return m.get(s);
    }
    public void removeUser(Socket s) {
        m.remove(s);
    }
}
SocketManager socketManager;
...
socketManager.setUser(socket, user);
这种方法的问题是元数据的生命周期需要与套接字的生命周期挂钩,但是除非准确地知道什么时候程序不再需要这个套接字,并记住从 Map 中删除相应的映射,否则,Socket  User 对象将会永远留在 Map 中,远远超过响应了请求和关闭套接字的时间。这会阻止 Socket  User 对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。除了最简单的情况,在几乎所有情况下找出什么时候 Socket 不再被程序使用是一件很烦人和容易出错的任务,需要人工对内存进行管理。

找出内存泄漏

程序有内存泄漏的第一个迹象通常是它抛出一个 OutOfMemoryError,或者因为频繁的垃圾收集而表现出糟糕的性能。幸运的是,垃圾收集可以提供能够用来诊断内存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 选项调用 JVM,那么每次 GC 运行时在控制台上或者日志文件中会打印出一个诊断信息,包括它所花费的时间、当前堆使用情况以及恢复了多少内存。记录 GC 使用情况并不具有干扰性,因此如果需要分析内存问题或者调优垃圾收集器,在生产环境中默认启用 GC 日志是值得的。
有工具可以利用 GC 日志输出并以图形方式将它显示出来,JTune 就是这样的一种工具(请参阅 参考资料)。观察 GC 之后堆大小的图,可以看到程序内存使用的趋势。对于大多数程序来说,可以将内存使用分为两部分:baseline 使用和 current load使用。对于服务器应用程序,baseline 使用就是应用程序在没有任何负荷、但是已经准备好接受请求时的内存使用,current load 使用是在处理请求过程中使用的、但是在请求处理完成后会释放的内存。只要负荷大体上是恒定的,应用程序通常会很快达到一个稳定的内存使用水平。如果在应用程序已经完成了其初始化并且负荷没有增加的情况下,内存使用持续增加,那么程序就可能在处理前面的请求时保留了生成的对象。
清单 2 展示了一个有内存泄漏的程序。MapLeaker 在线程池中处理任务,并在一个 Map 中记录每一项任务的状态。不幸的是,在任务完成后它不会删除那一项,因此状态项和任务对象(以及它们的内部状态)会不断地积累。
清单 2. 具有基于 Map 的内存泄漏的程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MapLeaker {
    public ExecutorService exec = Executors.newFixedThreadPool(5);
    public Map<Task, TaskStatus> taskStatus
        = Collections.synchronizedMap(new HashMap<Task, TaskStatus>());
    private Random random = new Random();
    private enum TaskStatus { NOT_STARTED, STARTED, FINISHED };
    private class Task implements Runnable {
        private int[] numbers = new int[random.nextInt(200)];
        public void run() {
            int[] temp = new int[random.nextInt(10000)];
            taskStatus.put(this, TaskStatus.STARTED);
            doSomeWork();
            taskStatus.put(this, TaskStatus.FINISHED);
        }
    }
    public Task newTask() {
        Task t = new Task();
        taskStatus.put(t, TaskStatus.NOT_STARTED);
        exec.execute(t);
        return t;
    }
}
图 1 显示 MapLeaker GC 之后应用程序堆大小随着时间的变化图。上升趋势是存在内存泄漏的警示信号。(在真实的应用程序中,坡度不会这么大,但是在收集了足够长时间的 GC 数据后,上升趋势通常会表现得很明显。)
图 1. 持续上升的内存使用趋势
确信有了内存泄漏后,下一步就是找出哪种对象造成了这个问题。所有内存分析器都可以生成按照对象类进行分解的堆快照。有一些很好的商业堆分析工具,但是找出内存泄漏不一定要花钱买这些工具 —— 内置的 hprof 工具也可完成这项工作。要使用hprof 并让它跟踪内存使用,需要以 -Xrunhprof:heap=sites 选项调用 JVM。
清单 3 显示分解了应用程序内存使用的 hprof 输出的相关部分。(hprof 工具在应用程序退出时,或者用 kill -3 或在 Windows 中按 Ctrl+Break 时生成使用分解。)注意两次快照相比,Map.EntryTask  int[] 对象有了显著增加。
请参阅 清单 3
清单 4 展示了 hprof 输出的另一部分,给出了 Map.Entry 对象的分配点的调用堆栈信息。这个输出告诉我们哪些调用链生成了 Map.Entry 对象,并带有一些程序分析,找出内存泄漏来源一般来说是相当容易的。
清单 4. HPROF 输出,显示 Map.Entry 对象的分配点
1
2
3
4
5
6
7
TRACE 300446:
    java.util.HashMap$Entry.<init>(<Unknown Source>:Unknown line)
    java.util.HashMap.addEntry(<Unknown Source>:Unknown line)
    java.util.HashMap.put(<Unknown Source>:Unknown line)
    java.util.Collections$SynchronizedMap.put(<Unknown Source>:Unknown line)
    com.quiotix.dummy.MapLeaker.newTask(MapLeaker.java:48)
    com.quiotix.dummy.MapLeaker.main(MapLeaker.java:64)

弱引用来救援了

SocketManager 的问题是 Socket-User 映射的生命周期应当与 Socket 的生命周期相匹配,但是语言没有提供任何容易的方法实施这项规则。这使得程序不得不使用人工内存管理的老技术。幸运的是,从 JDK 1.2 开始,垃圾收集器提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就可以帮助我们防止这种内存泄漏 —— 利用弱引用
弱引用是对一个对象(称为 referent)的引用的持有者。使用弱引用后,可以维持对 referent 的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个 referent 就会成为垃圾收集的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被清除。(只有弱引用的对象称为弱可及(weakly reachable)。)
WeakReference 的 referent 是在构造时设置的,在没有被清除之前,可以用 get() 获取它的值。如果弱引用被清除了(不管是 referent 已经被垃圾收集了,还是有人调用了 WeakReference.clear()),get() 会返回 null。相应地,在使用其结果之前,应当总是检查 get() 是否返回一个非 null 值,因为 referent 最终总是会被垃圾收集的。
用一个普通的(强)引用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命周期一样长。如果不小心,那么它可能就与程序的生命周期一样 —— 如果将一个对象放入一个全局集合中的话。另一方面,在创建对一个对象的弱引用时,完全没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另一种到达它的方法。
弱引用对于构造弱集合最有用,如那些在应用程序的其余部分使用对象期间存储关于这些对象的元数据的集合 —— 这就是SocketManager 类所要做的工作。因为这是弱引用最常见的用法,WeakHashMap 也被添加到 JDK 1.2 的类库中,它对键(而不是对值)使用弱引用。如果在一个普通 HashMap 中用一个对象作为键,那么这个对象在映射从 Map 中删除之前不能被回收,WeakHashMap 使您可以用一个对象作为 Map 键,同时不会阻止这个对象被垃圾收集。清单 5 给出了 WeakHashMap  get() 方法的一种可能实现,它展示了弱引用的使用:
清单 5. WeakReference.get() 的一种可能实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class WeakHashMap<K,V> implements Map<K,V> {
    private static class Entry<K,V> extends WeakReference<K>
      implements Map.Entry<K,V> {
        private V value;
        private final int hash;
        private Entry<K,V> next;
        ...
    }
    public V get(Object key) {
        int hash = getHash(key);
        Entry<K,V> e = getChain(hash);
        while (e != null) {
            K eKey= e.get();
            if (e.hash == hash && (key == eKey || key.equals(eKey)))
                return e.value;
            e = e.next;
        }
        return null;
    }
调用 WeakReference.get() 时,它返回一个对 referent 的强引用(如果它仍然存活的话),因此不需要担心映射在 while 循环体中消失,因为强引用会防止它被垃圾收集。WeakHashMap 的实现展示了弱引用的一种常见用法 —— 一些内部对象扩展 WeakReference。其原因在下面一节讨论引用队列时会得到解释。
在向 WeakHashMap 中添加映射时,请记住映射可能会在以后“脱离”,因为键被垃圾收集了。在这种情况下,get() 返回null,这使得测试 get() 的返回值是否为 null 变得比平时更重要了。

用 WeakHashMap 堵住泄漏

 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如清单 6 所示。(如果 SocketManager 需要线程安全,那么可以用 Collections.synchronizedMap() 包装 WeakHashMap)。当映射的生命周期必须与键的生命周期联系在一起时,可以使用这种方法。不过,应当小心不滥用这种技术,大多数时候还是应当使用普通的 HashMap 作为 Map 的实现。
清单 6. 用 WeakHashMap 修复 SocketManager
1
2
3
4
5
6
7
8
9
10
public class SocketManager {
    private Map<Socket,User> m = new WeakHashMap<Socket,User>();
     
    public void setUser(Socket s, User u) {
        m.put(s, u);
    }
    public User getUser(Socket s) {
        return m.get(s);
    }
}

引用队列

WeakHashMap 用弱引用承载映射键,这使得应用程序不再使用键对象时它们可以被垃圾收集,get() 实现可以根据WeakReference.get() 是否返回 null 来区分死的映射和活的映射。但是这只是防止 Map 的内存消耗在应用程序的生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象被收集后从 Map 中删除死项。否则,Map 会充满对应于死键的项。虽然这对于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存,因为即使键被收集了,Map.Entry 和值对象也不会被收集。
可以通过周期性地扫描 Map,对每一个弱引用调用 get(),并在 get() 返回 null 时删除那个映射而消除死映射。但是如果 Map有许多活的项,那么这种方法的效率很低。如果有一种方法可以在弱引用的 referent 被垃圾收集时发出通知就好了,这就是引用队列 的作用。
引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要方法。弱引用有两个构造函数:一个只取 referent 作为参数,另一个还取引用队列作为参数。如果用关联的引用队列创建弱引用,在 referent 成为 GC 候选对象时,这个引用对象(不是 referent)就在引用清除后加入 到引用队列中。之后,应用程序从引用队列提取引用并了解到它的 referent 已被收集,因此可以进行相应的清理活动,如去掉已不在弱集合中的对象的项。(引用队列提供了与 BlockingQueue 同样的出列模式 —— polled、timed blocking 和 untimed blocking。)
WeakHashMap 有一个名为 expungeStaleEntries() 的私有方法,大多数 Map 操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射。清单 7 展示了 expungeStaleEntries() 的一种可能实现。用于存储键-值映射的 Entry 类型扩展了 WeakReference,因此当 expungeStaleEntries() 要求下一个失效的弱引用时,它得到一个 Entry。用引用队列代替定期扫描内容的方法来清理 Map 更有效,因为清理过程不会触及活的项,只有在有实际加入队列的引用时它才工作。
清单 7. WeakHashMap.expungeStaleEntries() 的可能实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void expungeStaleEntries() {
Entry<K,V> e;
    while ( (e = (Entry<K,V>) queue.poll()) != null) {
        int hash = e.hash;
        Entry<K,V> prev = getChain(hash);
        Entry<K,V> cur = prev;
        while (cur != null) {
            Entry<K,V> next = cur.next;
            if (cur == e) {
                if (prev == e)
                    setChain(hash, next);
                else
                    prev.next = next;
                break;
            }
            prev = cur;
            cur = next;
        }
    }
}

结束语

弱引用和弱集合是对堆进行管理的强大工具,使得应用程序可以使用更复杂的可及性方案,而不只是由普通(强)引用所提供的“要么全部要么没有”可及性。下个月,我们将分析与弱引用有关的软引用,将分析在使用弱引用和软引用时,垃圾收集器的行为。

相关主题

您可以参阅本文在 developerWorks 全球站点上的 英文原文
JTune:免费 JTune 工具,可以使用 GC 日志并以图形方式显示堆大小、GC 持续时间和其他有用的内存管理数据。
关注性能: 调优垃圾收集”:Kirk Pepperdine 和 Jack Shirazi 展示了缓慢的内存泄漏最终对于垃圾收集器造成了无法承受的压力。
HPROF”:Sun 的这一篇文章描述了如何使用内置的 HPROF 分析工具。
Reference objects and garbage collection:Sun 的这篇文章是在 Reference 对象刚加入类库不久写的,描述了垃圾收集器如何处理 Reference 对象。
Java 技术专区:数百篇关于 Java 编程各个方面的文章。

原文地址:http://blog.51cto.com/rockybalboa/813161

 前言

若干年前看了Java的四种引用类型,只是简单知道了不同类型的作用,但对其实现原理一直未能想明白,本文尝试结合jdk,openjdk6的部分源码分析弱引用实现的原理,供大家参考,部分技术细节没有仔细研究,如有疑问欢迎留言讨论

 实例分析

我们以WeakHashMap的处理过程为例介绍一个weak reference的生命周期,首先我们调用WeakHashMap的put方法放入对象到Map中,WeakHashMap的Entry继承了WeakReference
 
  1. private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V> { 
  2.         private V value; 
  3.         private final int hash; 
  4.         private Entry<K,V> next; 
下面是put的部分代码
  1. Entry<K,V> e = tab[i]; 
  2.         tab[i] = new Entry<K,V>(k, value, queue, h, e); 
  3.         if (++size >= threshold) 
  4.             resize(tab.length * 2); 
  5.         return null; 
  6.     } 
注意new Entry传递了一个reference queue到构造函数中,此构造函数最终会调用Reference的构造函数
  1. Reference(T referent, ReferenceQueue<? super T> queue) { 
  2.     this.referent = referent; 
  3.     this.queue = (queue == null) ? ReferenceQueue.NULL : queue; 
  4.     } 
referent是我们之前传入的hashmap的key对象,queue的作用是用来读取referent被回收的weak reference,生产者是谁后续介绍,此时WeakHashMap中已经存在了一个对象,先将key对象的strong ref制空并尝试触发gc,比如使用System.gc()来显式的触发gc,然后调用WeakHashMap的size方法返回集合的个数,绝大多数情况下会是0,这个过程中发生了什么呢?
第一步,key没有可达的strong ref,仅仅存在一个weak reference的referent变量仍然指向了key,触发GC时,以openjdk6的parNew为例,jvm在young generation gc时会尝试获取Reference对象里的静态全局锁
 
  1. /* Object used to synchronize with the garbage collector.  The collector 
  2.      * must acquire this lock at the beginning of each collection cycle.  It is 
  3.      * therefore critical that any code holding this lock complete as quickly 
  4.      * as possible, allocate no new objects, and avoid calling user code. 
  5.      */ 
  6.     static private class Lock { }; 
  7.     private static Lock lock = new Lock(); 
在openjdk6里的部分源代码,完整代码请参考instanceRefKlass.cpp文件
  1. void instanceRefKlass::acquire_pending_list_lock(BasicLock *pending_list_basic_lock) { 
  2.   // we may enter this with pending exception set 
  3.   PRESERVE_EXCEPTION_MARK;  // exceptions are never thrown, needed for TRAPS argument 
  4.   Handle h_lock(THREAD, java_lang_ref_Reference::pending_list_lock()); 
  5.   ObjectSynchronizer::fast_enter(h_lock, pending_list_basic_lock, false, THREAD); 
  6.   assert(ObjectSynchronizer::current_thread_holds_lock( 
  7.            JavaThread::current(), h_lock), 
  8.          "Locking should have succeeded"); 
  9.   if (HAS_PENDING_EXCEPTION) CLEAR_PENDING_EXCEPTION; 
 此处代码在parNew gc时执行,目的就是尝试获取全局锁,在gc完成后,jvm会将key被回收的weak reference组成一个queue并赋值到Reference的pending属性然后释放锁,参考方法:
  1. void instanceRefKlass::release_and_notify_pending_list_lock( 
  2.   BasicLock *pending_list_basic_lock) { 
  3.   // we may enter this with pending exception set 
  4.   PRESERVE_EXCEPTION_MARK;  // exceptions are never thrown, needed for TRAPS argument 
  5.   // 
  6.   Handle h_lock(THREAD, java_lang_ref_Reference::pending_list_lock()); 
  7.   assert(ObjectSynchronizer::current_thread_holds_lock( 
  8.            JavaThread::current(), h_lock), 
  9.          "Lock should be held"); 
  10.   // Notify waiters on pending lists lock if there is any reference. 
  11.   if (java_lang_ref_Reference::pending_list() != NULL) { 
  12.     ObjectSynchronizer::notifyall(h_lock, THREAD); 
  13.   } 
  14.   ObjectSynchronizer::fast_exit(h_lock(), pending_list_basic_lock, THREAD); 
  15.   if (HAS_PENDING_EXCEPTION) CLEAR_PENDING_EXCEPTION; 
在一次gc后,Reference对象的pending属性不再为空,让我们看看Reference的部分代码
首先是pending属性的说明:
  1. /* List of References waiting to be enqueued.  The collector adds 
  2.  * References to this list, while the Reference-handler thread removes 
  3.  * them.  This list is protected by the above lock object. 
  4.  */ 
  5. private static Reference pending = null
接下来是Reference中的内部类ReferenceHandler,它继承了Thread,看看run方法的代码
  1. public void run() { 
  2.         for (;;) { 
  3.  
  4.         Reference r; 
  5.         synchronized (lock) { 
  6.             if (pending != null) { 
  7.             r = pending; 
  8.             Reference rn = r.next; 
  9.             pending = (rn == r) ? null : rn; 
  10.             r.next = r; 
  11.             } else { 
  12.             try { 
  13.                 lock.wait(); 
  14.             } catch (InterruptedException x) { } 
  15.             continue
  16.             } 
  17.         } 
  18.  
  19.         // Fast path for cleaners 
  20.         if (r instanceof Cleaner) { 
  21.             ((Cleaner)r).clean(); 
  22.             continue
  23.         } 
  24.  
  25.         ReferenceQueue q = r.queue; 
  26.         if (q != ReferenceQueue.NULL) q.enqueue(r); 
  27.         } 
  28.     } 
  29.     } 
一旦jvm notify了前面提到的锁,这个线程就被激活并开始执行,作用是将之前jvm赋值过来的pending对象中的WeakReference对象enqueue到指定的队列中,比如WeakHashMap内部定义的ReferenceQueue属性
此时map的queue中保存了referent已经被回收的WeakReference队列,也就是map的Entry对象,当调用size方法时,内部首先调用expungStaleEntries方法清除被回收掉的Entry,代码如下
 
  1. private void expungeStaleEntries() { 
  2.     Entry<K,V> e; 
  3.         while ( (e = (Entry<K,V>) queue.poll()) != null) { 
  4.             int h = e.hash; 
  5.             int i = indexFor(h, table.length); 
  6.  
  7.             Entry<K,V> prev = table[i]; 
  8.             Entry<K,V> p = prev; 
  9.             while (p != null) { 
  10.                 Entry<K,V> next = p.next; 
  11.                 if (p == e) { 
  12.                     if (prev == e) 
  13.                         table[i] = next; 
  14.                     else 
  15.                         prev.next = next; 
  16.                     e.next = null;  // Help GC 
  17.                     e.value = null//  "   " 
  18.                     size--; 
  19.                     break
  20.                 } 
  21.                 prev = p; 
  22.                 p = next; 
  23.             } 
  24.         } 
  25.     } 
ok,就这样map的废弃Entry被clear,size返回为0
经过简单的测试程序发现:
一次gc未必能完全回收所有的weak ref
weak对象也可能会出现在old generation
 
参考:
 
http://weblogs.java.net/blog/2006/05/04/understanding-weak-references
 http://stackoverflow.com/questions/154724/when-would-you-use-a-weakhashmap-or-a-weakreference

原文地址:http://blog.csdn.net/dagnet/article/details/6162168
经常会在linux的一些包上看见一些i386、i586之类的字样,只知道是一些cpu的指令集,现在整理一下和大家一起分享下:
IA32 : 32 bits Intel Architecture (32位带宽Intel构架)
IA64 : 64 bits Intel Architecture (64位带宽Intel构架)

i386 : Intel 386 ( 老的386机器,也泛指IA32体系的CPU)
i486 : Intel 486
i586 : Intel 586 ( Pentium ,K6 级别CPU )
i686 : Intel 686 ( Pentium II, Pentium III , Pentim 4, K7 级别CPU )

以上的86 也可以叫做 x86, 通称说 x86也是指 IA32构架CPU

x86是一个intel通用计算机系列的编号,也标识一套通用的计算机指令集合。

早期intel的CPU编号都是如8086,80286,由于这整个系列的CPU都是指令兼容的,所以都用X86来标识所使用的指令集合。

如今的奔腾,P2,P4,赛扬系列都是支持X86指令系统的,所以都属于X86家族。 

x86 family 6 model 65意思是这个CPU属于x86家族的第6代产品,采用65ns的工艺制造。 

AT/AT COMPATIBLE 这个的意思应该是说兼容AT/AT指令。

i386是指intel发布的通用处理器类型,适合386,486,586,686的CPU。有些rpm包还区分了这些类型。

下面是关于x86发展的一个简介

 
1978年6月8号,INTEL发布了其第一款16位的微处理器--8086,还有一句著名的广告语“开启了一个时代”。有点夸大其词?那是,不过也的确说的比较准。当8086的光环退去之后,其支撑架构--后来我们所熟知的x86也成为了最成功的业界技术标准。
 
“X86”是Intel和其他几家公司处理器所支持的一组机器指令集,它大致确定了芯片的使用规范。从8086到80186、80286、80386、80486,再到后来的奔腾系列以及现在的多核技术,都是使用一脉相承的x86指令集,既不断扩展又向后兼容。
 
在8086之后的30年间,x86家族横跨了桌面、服务器、便携式电脑,超级计算机等等。无数对手败倒在了其脚下,甚至是一些看似已成定局的领域。例如近年来被x86所吞掉的苹果电脑,原来一直使用的是PowerPC。
 
那么Intel的架构是怎么统治了电脑世界这么多年?让我们一起来看看。
 
最初在1971 年,Intel为一家日本计算器厂制造了Intel历史上的第一块处理器——4位的4004。很快,在1975年,Intel又推出了8位的处理器 8008和8080。8080处理器为Altair8800 PC所采用。顺便说一句,Altair8800 PC是一台邮件订单处理计算机。比尔·盖茨和保罗·艾伦建立微软公司的时候,就曾把Basic卖给Altair8800 PC。
 
3年以后,16位的8086初次登场。在80年代初,IBM公司选择了8086的衍生产品8088作为IBM PC的处理器。IBM的这一行为给X86带来了巨大发展,并且帮助它成为了行业标准——一直到今天。
 
Intel 执行副总裁Patrick Gelsinger说:“PC行业发展的革命性转折点是1985年32位处理器80386的推出,它推动了整个行业的发展。当时,X86需要从早期的16 位寻址空间进行升级。”人们问我们:“32位是什么?”我们说:“它是给微型计算机和中型计算机用的。”那时人们总会嘲笑们说这太浪费了,这些是没必要的事情。
 
当时IBM抛弃了386,因为当时没有32位的软件发挥出它的性能。IBM自己也开发了16位的操作系统叫做OS/2。
 
当时担任386设计小组成员的Patrick Gelsinger说:“IBM当时拥有自己的整个架构。它们有它们自己的应用程序,操作系统和硬件设计。当IBM开发下一代产品时,他们将是唯一能够给提供全套解决方案的,只是并不能保证下一代过渡的兼容性。”
 
“当386到来时候,所以的一切变了,”Gelsinger说,:“我们从一个垂直的行业到一个水平的行业,而且我们真的打开了新世界之门。”
 
386 之后,1989年486诞生了。由于当时数字不能作为商标,Intel从1993年开始改变了产品命名方法。第五代处理器被命名为Pentium而不是 586。数字命名产品转变为Pentium命名(比如Pentium Pro, Pentium II and Pentium D)。从那时开始,Intel在X86体系中,增加了低端的赛扬系列和高端的双核系列产品。所有的基于X86架构的芯片,开始于8086,一直延续到今天。当然他们的命名发生了变化,运算速度也有了惊人的提升。
 
为什么X86能一直成功,击退甚至完全打败其他的处理器架构?从一开始来说,X86的诞生就是在一个很恰当的时间点。1978年的时候,计算从巨大、昂贵的中型计算机转变为小型、便宜的微型计算机已经有几年了。台式电脑成为变革的前沿。更重要的是,X86证明了戈登·摩尔在1965年提出的一个定律。戈登.摩尔在后来成为Intel的主席和CEO。摩尔说,在成本不变的前提下,微处理每过 2年其运算速度会翻一番。他的预言,后来被成为摩尔定律,被证实是正确的。X86的发展道路越走越宽。X86处理器也从数据处理中心走向办公室和千家万户。并且8086以及它的后续产品一直与电脑行业两个大名鼎鼎的名字紧紧联系在一起。在1972年,比尔·盖茨和保罗·艾伦就尝试用性能很弱的8008 开发Basic编程语言,虽然后来失败了。但是他们最终在性能强劲一些的8080处理器上开发出了Basic语言,并在1975年把 Basic语言应用到Altair8800 PC。
 
这成为Intel和微软亲密关系的开始。微软从那开始,创造了一个巨大的软件帝国,并推动了整个行业的发展。在X86架构成功的过程中,可能没有比RISC处理器的影响更巨大的了。

x86是一个intel通用计算机系列的标准编号缩写,也标识一套通用的计算机指令集合,X与处理器没有任何关系,它是一个对所有*86系统的简单的通配符定义,例如:i386, 586,奔腾(pentium)。
  X64是微软公司为 AMD64 和 Intel EMT64 设备程序结构取的专有名称.
  也可以说X64是指寄存器为64位的CPU.X64是X86的发展,将寄存器扩展到了64位。
     在计算机架构中,64位整数、内存地址或其他数据单元,是指它们最高达到64位(8字节)宽。此外,64位CPU和算术逻辑单元架构是以寄存器、内存总线或者数据总线的大小为基准。
  64 位CPU在1960年代,便已存在于超级计算机,且早在1990年代,就有以 RISC 为基础的工作站和服务器。2003年才以 x86-64 和 64 位 PowerPC 处理器架构的形式引入到(在此之前是 32 位)个人计算机领域的主流。
     32 位计算机中的位数指的是CPU一次能处理的最大位数。32位计算机的CPU一次最多能处理32位数据,例如它的EAX寄存器就是32位的,当然32位计算机通常也可以处理16位和8位数据。在Intel由16位的286升级到386的时候,为了和16位系统兼容,它先推出的是386SX,这种CPU内部预算为32位,外部数据传输为16位。直到386DX以后,所有的CPU在内部和外部都是32位的了。有些人往往会弄不清在计算机中出现的“位”和 Byte,KB,MB等有何关系,而它们的关系是,8位等于一字节,即8bit=1B 。32位处理器每次处理 4Byte(32bit),同理,64位处理器每次处理 8Byte(64bit) 。

64位运算需要处理器和操作系统的支持,在个人电脑上的应用才刚刚开始,Windows vista 64bit 是比较完善的64位操作系统,64bit兼容绝大多数的32bit运算,所以32位程序绝大部分是可以在64bit的操作系统下运行的。随着计算机硬件的发展,64bit操作系统和64bit运算将成为主流。由于更多大型程序的出现,32bit系统开始无法适应时代的要求了。

个人理解
1.基于栈的指令架构和基于寄存器的指令架构,根据编译器的不同、cpu的不同而论。
2.基于栈的指令架构的执行程序使用的cpu指令比较少,编译出来的程序较小, 有操作数栈的概念,处理函数调用、递归简单明了,但是需要使用更多的指令,不能充分利用cpu的寄存器实现缓存,效率比寄存器架构慢,因为不考虑寄存器分配,因此实现简单。
3.基于寄存器的指令架构的执行程序使用的cpu指令比较多,编译出来的程序较大,无操作数栈的概念,使用的指令比较少,可充分利用寄存器来做缓存,更能实现乱序和并行,效率比栈架构高,但实现很复杂
4.使用栈架构的大部分是基于虚拟机的语言,如jvm、python、.net。使用寄存器架构的大部分是编译性的语言,如c/c++
5.jvm是基于栈的指令架构,但是并不就是没有使用寄存器,只是说用得相对较少,而且在不支持栈架构的处理器上还需要使用寄存器指令来对应java字节码指令。

堆栈的(Stack-based ) 和基于寄存器(Register-based) 的虚拟机区别
    地址:http://blog.csdn.net/u010385624/article/details/78883024
虚拟机可分为两种:基于堆栈的(Stack-based ) 和基于寄存器(Register-based) 的虚拟机。基于堆栈的虚拟机也定义了少量的寄存器,基于寄存器的虚拟机也有堆栈,其区别体现在它们提供的指令集体系结构(ISA ,Instruction Set Architecture) 。ISA 是处理器的一部分,对于编译器实现者和程序员是可见的,ISA 是硬件和软件之间的接口。基于堆栈的虚拟机的指令比基于寄存器的指令要小,因为在指令中不需要指定操作数。基于堆栈的虚拟机使用堆栈来保存中间结果、变量等,基于寄存器的虚拟机则支持寄存器的指令操作。基于堆栈的虚拟机需要用Push 、Pop 来传送数据,通常,完成同样的工作,基于寄存器的虚拟机所采用的指令数比基于堆栈的虚拟机采用的指令数目少,可以提高执行效率。例如,将语句 C=A+B转化为中间代码,如图3.3所示。 
 
这里写图片描述 
堆栈虚拟机指令很低级,基于寄存器的处理器有更强大的指令功能,而且易于调试。 
基于堆栈的处理器在处理函数调用、解决递归问题和切换上下文时简单明快。 
采用寄存器架构时,虚拟机需要经常保存和恢复寄存器中的内容,还要考虑对操作数的寻址问题等,因此,基于堆栈的虚拟机实现起来更简单,基于寄存器的虚拟机能提供更强大的指令集。 
大多数虚拟机是基于堆栈的,如Pascal’s P-machine 、Java 的JVM和微软的.Net 环境。当前流行的 Lua 脚本语言,5.0之前的版本也是基于堆栈的,5.0之后改为基于寄存器实现。虚拟机实现越复杂,解密者也就越难破解,但是其实现成本也高。 
基于寄存器的虚拟机需要用寄存器来保存中间结果、变量等,而基于堆栈的虚拟机使用堆栈即可。例如,针对同一条指令:Add,基于堆栈的处理器的首先从堆栈里Pop两个数,然后将两数相加,再把和Push到堆栈,Add指令只占用1个字节,而基于寄存器的处理器的对应指令为AddR1,R2,113,Add指令至少要占用4个字节。通过以上比较可以看出,基于堆栈的体系架构实现较为简单,但其运行效率不如基于寄存器的,而基于寄存器的体系结构在编译后的代码结构上又不如基于堆栈的简洁。对于同一个程序,为基于堆栈的机器编译出来的版本要比为基于寄存器的机器编译出来的版本小好几倍。 
基于堆栈的指令集能够让字节码更紧凑,可以提高Cache的利用率,而且实现起来也比基于寄存器的机器要简单。而基于寄存器的虚拟机虽然代码大小有所增加,但是带来的性能提升更加突出

基于栈的虚拟机 VS 基于寄存器的虚拟机
原文地址:http://blog.csdn.net/dashuniuniu/article/details/50347149
一直对虚拟机这个黑盒非常感兴趣,由于从前都是直接学习x86或者ARM这些实际的体系结构,什么寄存器、ALU、CPU、总线、乱序执行和Cache等相关的观念都已经烂熟于心。另外在学习C++或者C语言时,对函数调用栈帧非常熟悉,什么函数调用前压参、保存寄存器值、EBP、ESP或者函数返回值如何传递,更深层次的如对象的this指针如何传递,或者C++的RTTI以及C++内部的实现机制。但是对java里面的实现机制确实一知半解,为什么人们说对象都是分配在堆上(这个是Java语义模型决定的,C++是值模型,而Java是值模型和引用模型混合的,Builtin Type是值模型,UserDefined Type是引用模型,也就是分配在堆上 — 当然JVM应该有相应的优化措施,因为大量简单的小对象也分配在堆上的话,会增加GC的压力),JVM中的栈和C++中的栈不是一种概念。
刚接触到虚拟机这个概念的时候,有点儿茫然,虽然知道JVM相关的概念,什么字节码,什么JIT,什么GC啊,但是这些了解只是浅尝辄止,并没有什么实质性的认识。再遇上lua或者python的实现机制,更是云里雾里。
那么虚拟机到底是什么,是怎么工作的,为什么要设计成这样?在这篇文章中,我就简单叙述一下最近对虚拟机的理解。

什么是虚拟机

虚拟机是借助于操作系统对物理机器的一种模拟。但是我们今天所讲述的虚拟机概念比较狭义,与vmware或者virtual-box不同,而是针对具体语言所实现的虚拟机。例如在JVM或者CPython中,JAVA或者python源码会被编译成相关字节码,然后在对应虚拟机上运行,JVM或CPython会对这些字节码进行取指令,译码,执行,结果回写等操作,这些步骤和真实物理机器上的概念都很相似。相对应的二进制指令是在物理机器上运行,物理机器从内存中取指令,通过总线传输到CPU,然后译码、执行、结果存储。
虚拟机为了能够执行字节码,需要模拟出物理CPU能够执行的相关操作,与虚拟机实现相关的概念如下:
(1)将源码编译成VM所能执行的具体字节码。 
(2)字节码格式(指令格式),例如三元式,树还是前缀波兰式。 
(3)函数调用相关的栈结构,函数的入口,出口,返回以及如何传参。还有为了能够顺利返回所需的相关栈帧信息如何布置。 
(4)一个“指令指针”,指向下一条待执行的指令(内存中),对应物理机器的EIP。 
(5)一个虚拟“CPU”-指令调度器,
获取下一条指令
对操作数进行解码
执行这条指令
这三点是解释器执行字节码最重要的开销。

虚拟机的实现方式

如今虚拟机的实现方式有两种,基于栈的和基于寄存器的,这两种实现方式各有优劣,也都有标志性的产品。基于栈的虚拟机,有JVM,CPython以及.Net CLR。基于寄存器的,有Dalvik以及Lua5.0,另外Perl听说也要改为基于寄存器方式。无论这两种方式实现机制如何,都要实现以下几点:
取指令,其中指令来源于内存
译码,决定指令类型(执行何种操作)。另外译码的过程要包括从内存中取操作数
执行。指令译码后,被虚拟机执行(其实最终都会借助于物理机资源)
存储计算结果
其实这和物理机CPU的执行是很相似的,都包括取值,译码,执行,回写等步骤。但是不同的一点是虚拟机应该模仿不出流水线,例如在当前指令译码完成之后,CPU中的译码部件处于空闲状态,可以用来对下一条指令进行译码,所以流水线有多少级就相当于可以并行执行多少指令。当然中间还有些指令相关和乱序的概念,这里就不详说了。
下图中一个典型的指令流水线结构,由于虚拟机在操作系统上通过程序模拟,遵循冯诺依曼结构顺序执行的,应该很难实现出流水线结构。

基于栈的虚拟机

基于栈的虚拟机有一个操作数栈的概念,虚拟机在进行真正的运算时都是直接与操作数栈(operand stack)进行交互,不能直接操作内存中数据(其实这句话不严谨的,虚拟机的操作数栈也是布局在内存上的),也就是说不管进行何种操作都要通过操作数栈来进行,即使是数据传递这种简单的操作。这样做的直接好处就是虚拟机可以无视具体的物理架构,特别是寄存器。但缺点也显而易见,就是速度慢,因为无论什么操作都要通过操作数栈这一结构。
由于执行时默认都是从操作数栈上取数据,那么就无需指定操作数。例如,x86汇编”ADD EAX, EBX”,就需要指定这次运算需要从什么地方取操作数,执行完结果存放在何处。但是基于栈的虚拟机的指令就无需指定,例如加法操作就一个简单的”Add”就可以了,因为默认操作数存放在操作数栈上,直接从操作数栈上pop出两条数据直接执行加法运算,运算后的结果默认存放在栈顶。其中操作数栈(operand stack)的深度由编译器静态确定,方便给栈帧预分配空间。这个和不能再栈上定义变长数组相似(其实这句话不严谨的,栈上分配变长数组,需要编译器的支持,分配在栈顶),由于局部变量的地址只能在编译期(compile time)确定针对当前栈帧的offset,如果中间有一个变量是一个变长数组的话,那么后面变量的offset就无法确定了(vector的数据是分配在堆上的,自己控制)。
例如执行”a = b + c”,在基于栈的虚拟机上字节码指令如下所示:
I1: LOAD CI2: LOAD BI3: ADD I4: STORE A
1
2
3
4
由于操作数都是隐式地,所以指令可以做的很短,一般都是一个或者两个字节。但是显而易见就是指令条数会显著增加。而基于寄存器虚拟机执行该操作只有一条指令,
I1: add a, b, c
1
其中a,b,c都是虚拟寄存器。操作数栈上的变化如下图所示:
首先从符号表上读取数据压入操作数栈,
然后从栈中弹出操作数执行加法运算,这步操作有物理机器执行,如下图所示:
从图示中可以看出,数据从局部变量表中还要经过一次操作数栈的操作,注意操作数栈和局部变量表都是存放在内存上,内存到内存的数据传输在x86的机器上都是要经过一次数据总线传输的。可以得出一次简单的加法基本上需要9次数据传输,想想都很慢。
但是基于栈的虚拟机优点就是可移植,寄存器由硬件直接提供。使用栈架构的指令集,用户程序(编译后的字节码)不会直接使用硬件中的寄存器,同时为了提高运行时的速度,可以将一些访问比较频繁的数据存放到寄存器中以获取尽量好的性能。另外,基于栈的虚拟机中指令更加紧凑,一个字节或者两个字节即可存储,同时编译器实现也比较简单,不用进行寄存器分配。寄存器分配是一门大学问

基于寄存器的虚拟机

前面提到过基于栈的虚拟机,这里我们简要介绍一下基于寄存器的虚拟机运行机制。
基于寄存器的虚拟机中没有操作数栈的概念,但是有很多虚拟寄存器,一般情况下这些寄存器(操作数)都是别名,需要执行引擎对这些寄存器(操作数)的解析,找出操作数的具体位置,然后取出操作数进行运算。
既然是虚拟寄存器,那么肯定不在CPU中(想想也不应该在CPU中,虚拟机的根本目的就是跨平台和兼容性),其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。
新的虚拟机也用栈分配活动记录,寄存器就在该活动记录中。当进入Lua程序的函数体时,函数从栈中分配一个足以容纳该函数所有寄存器的活动记录。函数的所有局部变量都各占据一个寄存器。因此,存取局部变量是相当高效的。
上面就是Lua虚拟机对寄存器的相关描述,示意图如下:
从上图中我们可以看到,其实“寄存器”的概念只是当前栈帧中一块连续的内存区域。这些数据在运算的时候,直接送入物理CPU进行计算,无需再传送到operand stack上然后再进行运算。例如”ADD R3, R2, R1”的示意图就如下所示:
其实”ADD R3, R2, R1”还要经过译码的一个过程,当然当前这条指令的种类和操作数由虚拟机进行解释。后面我们会看到,在有些实现中,有一个很大的switch-case来进行指令的分派及真正的运算过程。
下图是Lua虚拟机的一些指令,该图片来自这篇文章,中译文这里
使用寄存器式虚拟机没有基于栈的虚拟机在拷贝数据而使用的大量的出入栈(push/pop)指令。同时指令更紧凑更简洁。但是由于显示指定了操作数,所以基于寄存器的代码会比基于栈的代码要大,但是由于指令数量的减少,其实没有大多少。

栈式虚拟机 VS 寄存器式虚拟机

(1)指令条数:栈式虚拟机多 
(2)代码尺寸:栈式虚拟机 
(3)移植性:栈式虚拟机移植性更好 
(4)指令优化:寄存器式虚拟机更能优化
栈式 VS 寄存器式
对比
指令条数
栈式 > 寄存器式
代码尺寸
栈式 < 寄存器式
移植性
栈式优于寄存器式
指令优化
栈式更不易优化
解释器执行速度
栈式解释器速度稍慢
代码生成难度
栈式简单
简易实现中的数据移动次数
栈式移动次数多
解释器最重要的开销在于指令调度(instruction dispatch),指令调度主要操作包括从内存中取出指令,然后跳转到解释器相对应的代码段,然后执行这条指令。其中一个简易实现就是使用switch-based的方式来进行,这种方式简单易实现,另外任何语言都有相应的switch语句。switch-based的指令调度,通过一个死循环不断的从内存取出指令来执行,针对不同的指令选择不同的执行方式。
一种JVM基于SBD实现方式如下图所示:
 
注:图片来自这里
这种方式实现加单,代码移植性好,但是有一个缺点就是分支预测失效的概率比较高。
现在的CPU都是基于流水线结构的,间接跳转指令的跳转结果需要等到执行级才能知晓,如果预测失败需要排空流水线,流水线级数越多分支预测失败导致流水线排空的时间越长。
由于编译后的指令是随机的,不太可能提取出预测模式。《》

CPU架构中的栈和寄存器?

  
栈和寄存器有一个简单的逻辑关系。寄存器是最顶 stack frame 的随机访问方式。
什么是寄存器 « 技术奇异点
CPU 的寄存器是对逻辑寄存器的硬件优化。
基于栈的虚拟机就是每条指令只能访问 top-most 的一到两个 stack entry。基于寄存器的虚拟机是每条指令可以自由访问 top-most stack frame 里的任意 stack entry。
把虚拟机的定义放到 CPU 上来说,没有基于栈的 CPU。因为几乎所有 CPU 都允许访问栈的任何一个 entry。只能说 compiler 是否选择这些指令。不过,如果 CPU 的硬件寄存器比较少,top-most stack frame 都在主存里,习惯上就说是基于栈了。不过这个概念已经和虚拟机的分类法没什么关系了。
RISC CPU 一般硬件寄存器比较多。也就是说有更多的 top-most stack frames 可以做硬件优化。CISC 只是更少的硬件优化。

作者:冯东
链接:https://www.zhihu.com/question/27683261/answer/37800701
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


其实是指令系统分成堆栈型和寄存器型。不光这两种,指令系统共有四种分类,堆栈型,累加器型,寄存器-存储器型和寄存器-寄存器型。分类的依据是操作数的来源。堆栈型默认的操作数都在栈顶,累加器型默认一个操作数是累加器,寄存器-存储器型的操作数可以是寄存器或者内存,寄存器-寄存器型除了访存指令,操作数都是寄存器。早期的计算机结构简单,为了简化指令,所以经常使用堆栈或者累加器型的指令,如今的CPU早就有足够的晶体管来支持复杂设计,为了性能着想,大量使用寄存器型的指令,原因在于寄存器离CPU最近,所以延时最短,取指最快,有利于主频提高。再次,寄存器的相关性容易判断,有利于实现指令流水,多发射和乱序执行,对提高并发有极大的好处。intel的X86还保留有累加器指令和堆栈型指令,这是为了历史兼容。很多现今的RISC处理器,除了load和store指令访存外,只支持对寄存器操作,不支持对堆栈以及内存的直接操作,所以这种体系结构又叫做load-store架构。

作者:龚黎明
链接:https://www.zhihu.com/question/27683261/answer/37753090
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


x86一开始并没有使用太多的通用寄存器,原因之一(注意,只是之一)是当时的编译器无力进行寄存器分配,让编译器自动决定程序中众多变量哪些应该装入寄存器哪些应该换出、哪些变量应该映射到同一个寄存器上,并不是一件易事,JVM采用堆栈结构的原因之一就是不信任编译器的寄存器分配能力,转而使用堆栈结构,躲开寄存器分配的难题。

到80年代早期,IBM的G. J. Chaitin公开了他们的图染色寄存器分配算法之后,编译器的分配能力获得长足进步,形成了现在这样的编译器主导的寄存器分配格局,这个寄存器分配算法是IBM内部进行的一个RISC早期试验项目的一部分,但是我并没有看到有公开资料表明他们当时已经意识到RISC的寄存器数目将带来的性能暗示,而在图着色算法走向公开、成熟之前,RISC的理念就已经定型了,所以我也不认为RISC构建过程中有非常注重寄存器数目的考量,寄存器数目只是RISC发展中一个有意无意的副产品。当时RISC的主力推手之一,我们这个领域的泰山北斗David Patterson与DEC VAX团队的两位架构师Douglas W. Clark and William D. Strecker在《体系结构通讯》(CAN)上刊文论战时也并未以寄存器数目优势来说事。(推荐拙作一篇,RISC诞生与发展的缩影

作者:知乎用户
链接:https://www.zhihu.com/question/27683261/answer/37785620
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。




(Disclaimer:如果需要转载请先与我联系;文中图片请不要直接链接 
作者:@RednaxelaFX -> http://rednaxelafx.iteye.com 

大前天收到一条PM: 
引用
你好,很冒昧的向你发短消息,我现在在看JS引擎,能过看博客发现你对js engine很了解,我想请教一下你 基于栈的解析器与基于寄存器的解析器有什么同,javascriptcore是基于寄存器的,V8是基于栈的,能不能说一下这两者有什么一样吗?能推荐一点资料吗?谢谢。

我刚收到的时候很兴奋,就开始写回复。写啊写发觉已经比我平时发的帖还要长了,想着干脆把回复直接发出来好了。于是下面就是回复: 

你好 ^ ^ 很抱歉拖了这么久才回复。码字和画图太耗时间了。 
别说冒昧了,我只是个普通的刚毕业的学生而已,担当不起啊 =_=|||| 
而且我也不敢说“很”了解,只是有所接触而已。很高兴有人来一起讨论JavaScript引擎的设计与实现,总觉得身边对这个有兴趣的人不多,或者是很少冒出来讨论。如果你发个帖或者blog来讨论这方面的内容我也会很感兴趣的~

想拿出几点来讨论一下。上面提出的问题我希望能够一一给予回答,不过首先得做些铺垫。 
另外先提一点:JavaScriptCore从SquirrelFish版开始是“基于寄存器”的,V8则不适合用“基于栈”或者“基于寄存器”的说法来描述。 

1、解析器与解释器 

解析器是parser,而解释器是interpreter。两者不是同一样东西,不应该混用。 

前者是编译器/解释器的重要组成部分,也可以用在IDE之类的地方;其主要作用是进行语法分析,提取出句子的结构。广义来说输入一般是程序的源码,输出一般是语法树(syntax tree,也叫parse tree等)抽象语法树(abstract syntax tree,AST)。进一步剥开来,广义的解析器里一般会有扫描器(scanner,也叫tokenizer或者lexical analyzer,词法分析器),以及狭义的解析器(parser,也叫syntax analyzer,语法分析器)。扫描器的输入一般是文本,经过词法分析,输出是将文本切割为单词的流。狭义的解析器输入是单词的流,经过语法分析,输出是语法树或者精简过的AST。 
(在一些编译器/解释器中,解析也可能与后续的语义分析、代码生成或解释执行等步骤融合在一起,不一定真的会构造出完整的语法树。但概念上说解析器就是用来抽取句子结构用的,而语法树就是表示句子结构的方式。关于边解析边解释执行的例子,可以看看这帖的计算器。) 
举例:将i = a + b * c作为源代码输入到解析器里,则广义上的解析器的工作流程如下图: 
 
其中词法分析由扫描器完成,语法分析由狭义的解析器完成。 
(嗯,说来其实“解析器”这词还是按狭义用法比较准确。把扫描器和解析器合起来叫解析器总觉得怪怪的,但不少人这么用,这里就将就下吧 =_= 
不过近来“scannerless parsing”也挺流行的:不区分词法分析与语法分析,没有单独的扫描器,直接用解析器从源码生成语法树。这倒整个就是解析器了,没狭不狭义的问题) 

后者则是实现程序执行的一种实现方式,与编译器相对。它直接实现程序源码的语义,输入是程序源码,输出则是执行源码得到的计算结果;编译器的输入与解释器相同,而输出是用别的语言实现了输入源码的语义的程序。通常编译器的输入语言比输出语言高级,但不一定;也有输入输出是同种语言的情况,此时编译器很可能主要用于优化代码。 
举例:把同样的源码分别输入到编译器与解释器中,得到的输出不同: 
 
值得留意的是,编译器生成出来的代码执行后的结果应该跟解释器输出的结果一样——它们都应该实现源码所指定的语义。 

在很多地方都看到解析器与解释器两个不同的东西被混为一谈,感到十分无奈。 
最近某本引起很多关注的书便在开篇给读者们当头一棒,介绍了“JavaScript解析机制”。“编译”和“预处理”也顺带混为一谈了,还有“预编译” 0_0 
我一直以为“预编译”应该是ahead-of-time compilation的翻译,是与“即时编译”(just-in-time compilation,JIT)相对的概念。另外就是PCH(precompile header)这种用法,把以前的编译结果缓存下来称为“预编译”。把AOT、PCH跟“预处理”(preprocess)混为一谈真是诡异。算了,我还是不要淌这浑水的好……打住。 


2、“解释器”到底是什么?“解释型语言”呢? 

很多资料会说,Python、Ruby、JavaScript都是“解释型语言”,是通过解释器来实现的。这么说其实很容易引起误解:语言一般只会定义其抽象语义,而不会强制性要求采用某种实现方式。 
例如说C一般被认为是“编译型语言”,但C的解释器也是存在的,例如Ch。同样,C++也有解释器版本的实现,例如Cint 
一般被称为“解释型语言”的是主流实现为解释器的语言,但并不是说它就无法编译。例如说经常被认为是“解释型语言”的Scheme就有好几种编译器实现,其中率先支持R6RS规范的大部分内容的是Ikarus,支持在x86上编译Scheme;它最终不是生成某种虚拟机的字节码,而是直接生成x86机器码。 

解释器就是个黑箱,输入是源码,输出就是输入程序的执行结果,对用户来说中间没有独立的“编译”步骤。这非常抽象,内部是怎么实现的都没关系,只要能实现语义就行。你可以写一个C语言的解释器,里面只是先用普通的C编译器把源码编译为in-memory image,然后直接调用那个image去得到运行结果;用户拿过去,发现直接输入源码可以得到源程序对应的运行结果就满足需求了,无需在意解释器这个“黑箱子”里到底是什么。 
实际上很多解释器内部是以“编译器+虚拟机”的方式来实现的,先通过编译器将源码转换为AST或者字节码,然后由虚拟机去完成实际的执行。所谓“解释型语言”并不是不用编译,而只是不需要用户显式去使用编译器得到可执行代码而已。 

那么虚拟机(virtual machine,VM)又是什么?在许多不同的场合,VM有着不同的意义。如果上下文是Java、Python这类语言,那么一般指的是高级语言虚拟机(high-level language virtual machine,HLL VM),其意义是实现高级语言的语义。VM既然被称为“机器”,一般认为输入是满足某种指令集架构(instruction set architecture,ISA)的指令序列,中间转换为目标ISA的指令序列并加以执行,输出为程序的执行结果的,就是VM。源与目标ISA可以是同一种,这是所谓same-ISA VM。 
前面提到解释器中的编译器的输出可能是AST,也可能是字节码之类的指令序列;一般会把执行后者的程序称为VM,而执行前者的还是笼统称为解释器或者树遍历式解释器(tree-walking interpreter)。这只是种习惯而已,并没有多少确凿的依据。只不过线性(相对于树形)的指令序列看起来更像一般真正机器会执行的指令序列而已。 
其实我觉得把执行AST的也叫VM也没啥大问题。如果认同这个观点,那么把DLR看作一种VM也就可以接受了——它的“指令集”就是树形的Expression Tree。 

VM并不是神奇的就能执行代码了,它也得采用某种方式去实现输入程序的语义,并且同样有几种选择:“编译”,例如微软的.NET中的CLR;“解释”,例如CPython、CRuby 1.9,许多老的JavaScript引擎等;也有介于两者之间的混合式,例如Sun的JVM,HotSpot。如果采用编译方式,VM会把输入的指令先转换为某种能被底下的系统直接执行的形式(一般就是native code),然后再执行之;如果采用解释方式,则VM会把输入的指令逐条直接执行。 
换个角度说,我觉得采用编译和解释方式实现虚拟机最大的区别就在于是否存下目标代码:编译的话会把输入的源程序以某种单位(例如基本块/函数/方法/trace等)翻译生成为目标代码,并存下来(无论是存在内存中还是磁盘上,无所谓),后续执行可以复用之;解释的话则是把源程序中的指令逐条解释,不生成也不存下目标代码,后续执行没有多少可复用的信息。有些稍微先进一点的解释器可能会优化输入的源程序,把满足某些模式的指令序列合并为“超级指令”;这么做就是朝着编译的方向推进。后面讲到解释器的演化时再讨论超级指令吧。 

如果一种语言的主流实现是解释器,其内部是编译器+虚拟机,而虚拟机又是采用解释方式实现的,或者内部实现是编译器+树遍历解释器,那它就是名副其实的“解释型语言”。如果内部用的虚拟机是用编译方式实现的,其实跟普遍印象中的“解释器”还是挺不同的…… 

可以举这样一个例子:ActionScript 3,一般都被认为是“解释型语言”对吧?但这种观点到底是把FlashPlayer整体看成一个解释器,因而AS3是“解释型语言”呢?还是认为FlashPlayer中的虚拟机采用解释执行方案,因而AS3是“解释型语言”呢? 
其实Flash或Flex等从AS3生成出来的SWF文件里就包含有AS字节码(ActionScript Byte Code,ABC)。等到FlashPlayer去执行SWF文件,或者说等到AVM2(ActionScript Virtual Machine 2)去执行ABC时,又有解释器和JIT编译器两种实现。这种需要让用户显式进行编译步骤的语言,到底是不是“解释型语言”呢?呵呵。所以我一直觉得“编译型语言”跟“解释型语言”的说法太模糊,不太好。 
有兴趣想体验一下从命令行编译“裸”的AS3文件得到ABC文件,再从命令行调用AVM2去执行ABC文件的同学,可以从这帖下载我之前从源码编译出来的AVM2,自己玩玩看。例如说要编译一个名为test.as的文件,用下列命令: 
Command prompt代码  
  1. java -jar asc.jar -import builtin.abc -import toplevel.abc test.as  

就是用ASC将test.as编译,得到test.abc。接着用: 
Command prompt代码  
  1. avmplus test.abc  

就是用AVM2去执行程序了。很生动的体现出“编译器+虚拟机”的实现方式。 
这个“裸”的AVM2没有带Flash或Flex的类库,能用的函数和类都有限。不过AS3语言实现是完整的。可以用print()函数来向标准输出流写东西。 
Well……其实写Java程序不也是这样么?现在也确实还有很多人把Java称为“解释型语言”,完全无视Java代码通常是经过显式编译步骤才得到.class文件,而有些JVM是采用纯JIT编译方式实现的,内部没解释器,例如JRockitMaxine VMJikes RVM。我愈发感到“解释型语言”是个应该避开的用语 =_= 

关于虚拟机,有本很好的书绝对值得一读,《虚拟机——系统与进程的通用平台》(Virtual Machines: Versatile Platforms for Systems and Processes)。国内有影印版也有中文版,我是读了影印版,不太清楚中文版的翻译质量如何。据说翻译得还行,我无法印证。 


3、基于栈与基于寄存器的指令集架构 

用C的语法来写这么一个语句: 
C代码  
  1. a = b + c;  

如果把它变成这种形式: 
add a, b, c 
那看起来就更像机器指令了,对吧?这种就是所谓“三地址指令”(3-address instruction),一般形式为: 
op dest, src1, src2 
许多操作都是二元运算+赋值。三地址指令正好可以指定两个源和一个目标,能非常灵活的支持二元操作与赋值的组合。ARM处理器的主要指令集就是三地址形式的。 

C里要是这样写的话: 
C代码  
  1. a += b;  

变成: 
add a, b 
这就是所谓“二地址指令”,一般形式为: 
op dest, src 
它要支持二元操作,就只能把其中一个源同时也作为目标。上面的add a, b在执行过后,就会破坏a原有的值,而b的值保持不变。x86系列的处理器就是二地址形式的。 

上面提到的三地址与二地址形式的指令集,一般就是通过“基于寄存器的架构”来实现的。例如典型的RISC架构会要求除load和store以外,其它用于运算的指令的源与目标都要是寄存器。 

显然,指令集可以是任意“n地址”的,n属于自然数。那么一地址形式的指令集是怎样的呢? 
想像一下这样一组指令序列: 
add 5 
sub 3 
这只指定了操作的源,那目标是什么?一般来说,这种运算的目标是被称为“累加器”(accumulator)的专用寄存器,所有运算都靠更新累加器的状态来完成。那么上面两条指令用C来写就类似: 
C代码  
  1. acc += 5;  
  2. acc -= 3;  

只不过acc是“隐藏”的目标。基于累加器的架构近来比较少见了,在很老的机器上繁荣过一段时间。 

那“n地址”的n如果是0的话呢? 
看这样一段Java字节码: 
Java bytecode代码  
  1. iconst_1  
  2. iconst_2  
  3. iadd  
  4. istore_0  

注意那个iadd(表示整型加法)指令并没有任何参数。连源都无法指定了,零地址指令有什么用?? 
零地址意味着源与目标都是隐含参数,其实现依赖于一种常见的数据结构——没错,就是栈。上面的iconst_1、iconst_2两条指令,分别向一个叫做“求值栈”(evaluation stack,也叫做operand stack“操作数栈”或者expression stack“表达式栈”)的地方压入整型常量1、2。iadd指令则从求值栈顶弹出2个值,将值相加,然后把结果压回到栈顶。istore_0指令从求值栈顶弹出一个值,并将值保存到局部变量区的第一个位置(slot 0)。 
零地址形式的指令集一般就是通过“基于栈的架构”来实现的。请一定要注意,这个栈是指“求值栈”,而不是与系统调用栈(system call stack,或者就叫system stack)。千万别弄混了。有些虚拟机把求值栈实现在系统调用栈上,但两者概念上不是一个东西。 

由于指令的源与目标都是隐含的,零地址指令的“密度”可以非常高——可以用更少空间放下更多条指令。因此在空间紧缺的环境中,零地址指令是种可取的设计。但零地址指令要完成一件事情,一般会比二地址或者三地址指令许多更多条指令。上面Java字节码做的加法,如果用x86指令两条就能完成了: 
X86 asm代码  
  1. mov  eax, 1  
  2. add  eax, 2  

(好吧我犯规了,istore_0对应的保存我没写。但假如局部变量比较少的话也不必把EAX的值保存(“溢出”,register spilling)到调用栈上,就这样吧 =_= 
其实就算把结果保存到栈上也就是多一条指令而已……) 

一些比较老的解释器,例如CRuby在1.9引入YARV作为新的VM之前的解释器,还有SquirrleFish之前的老JavaScriptCore以及它的前身KJS,它们内部是树遍历式解释器;解释器递归遍历树,树的每个节点的操作依赖于解释其各个子节点返回的值。这种解释器里没有所谓的求值栈,也没有所谓的虚拟寄存器,所以不适合以“基于栈”或“基于寄存器”去描述。 

而像V8那样直接编译JavaScript生成机器码,而不通过中间的字节码的中间表示的JavaScript引擎,它内部有虚拟寄存器的概念,但那只是普通native编译器的正常组成部分。我觉得也不应该用“基于栈”或“基于寄存器”去描述它。
V8在内部也用了“求值栈”(在V8里具体叫“表达式栈”)的概念来简化生成代码的过程,在编译过程中进行“抽象解释”,使用所谓“虚拟栈帧”来记录局部变量与求值栈的状态;但在真正生成代码的时候会做窥孔优化,消除冗余的push/pop,将许多对求值栈的操作转变为对寄存器的操作,以此提高代码质量。于是最终生成出来的代码看起来就不像是基于栈的代码了。 

关于JavaScript引擎的实现方式,下文会再提到。 


4、基于栈与基于寄存器架构的VM,用哪个好? 

如果是要模拟现有的处理器,那没什么可选的,原本处理器采用了什么架构就只能以它为源。但HLL VM的架构通常可以自由构造,有很大的选择余地。为什么许多主流HLL VM,诸如JVM、CLI、CPython、CRuby 1.9等,都采用了基于栈的架构呢?我觉得这有三个主要原因: 

·实现简单 
由于指令中不必显式指定源与目标,VM可以设计得很简单,不必考虑为临时变量分配空间的问题,求值过程中的临时数据存储都让求值栈包办就行。 
更新:回帖中cscript指出了这句不太准确,应该是针对基于栈架构的指令集生成代码的编译器更容易实现,而不是VM更容易实现。 

·该VM是为某类资源非常匮乏的硬件而设计的 
这类硬件的存储器可能很小,每一字节的资源都要节省。零地址指令比其它形式的指令更紧凑,所以是个自然的选择。 

·考虑到可移植性 
处理器的特性各个不同:典型的CISC处理器的通用寄存器数量很少,例如32位的x86就只有8个32位通用寄存器(如果不算EBP和ESP那就是6个,现在一般都算上);典型的RISC处理器的各种寄存器数量多一些,例如ARM有16个32位通用寄存器,Sun的SPARC在一个寄存器窗口里则有24个通用寄存器(8 in,8 local,8 out)。 
假如一个VM采用基于寄存器的架构(它接受的指令集大概就是二地址或者三地址形式的),为了高效执行,一般会希望能把源架构中的寄存器映射到实际机器上寄存器上。但是VM里有些很重要的辅助数据会经常被访问,例如一些VM会保存源指令序列的程序计数器(program counter,PC),为了效率,这些数据也得放在实际机器的寄存器里。如果源架构中寄存器的数量跟实际机器的一样,或者前者比后者更多,那源架构的寄存器就没办法都映射到实际机器的寄存器上;这样VM实现起来比较麻烦,与能够全部映射相比效率也会大打折扣。像Dalvik VM的解释器实现,就是把虚拟寄存器全部映射到栈帧(内存)里的,这跟把局部变量区与操作数栈都映射到内存里的JVM解释器实现相比实际区别不太大。 

如果一个VM采用基于栈的架构,则无论在怎样的实际机器上,都很好实现——它的源架构里没有任何通用寄存器,所以实现VM时可以比较自由的分配实际机器的寄存器。于是这样的VM可移植性就比较高。作为优化,基于栈的VM可以用编译方式实现,“求值栈”实际上也可以由编译器映射到寄存器上,减轻数据移动的开销。 

回到主题,基于栈与基于寄存器的架构,谁更快?看看现在的实际处理器,大多都是基于寄存器的架构,从侧面反映出它比基于栈的架构更优秀。 
而对于VM来说,源架构的求值栈或者寄存器都可能是用实际机器的内存来模拟的,所以性能特性与实际硬件又有点不同。一般认为基于寄存器的架构对VM来说也是更快的,原因是:虽然零地址指令更紧凑,但完成操作需要更多的load/store指令,也意味着更多的指令分派(instruction dispatch)次数与内存访问次数;访问内存是执行速度的一个重要瓶颈,二地址或三地址指令虽然每条指令占的空间较多,但总体来说可以用更少的指令完成操作,指令分派与内存访问次数都较少。 
这方面有篇被引用得很多的论文讲得比较清楚,Virtual Machine Showdown: Stack Versus Registers,是在VEE 2005发表的。VEE是Virtual Execution Environment的缩写,是ACM下SIGPLAN组织的一个会议,专门研讨虚拟机的设计与实现的。可以去找找这个会议往年的论文,很多都值得读。 


5、树遍历解释器图解 

在演示基于栈与基于寄存器的VM的例子前,先回头看看更原始的解释器形式。 
前面提到解析器的时候用了i = a + b * c的例子,现在让我们来看看由解析器生成的AST要是交给一个树遍历解释器,会如何被解释执行呢? 

用文字说不够形象,还是看图吧: 
 
这是对AST的后序遍历:假设有一个eval(Node n)函数,用于解释AST上的每个节点;在解释一个节点时如果依赖于子树的操作,则对子节点递归调用eval(Node n),从这些递归调用的返回值获取需要的值(或副作用)——也就是说子节点都eval好了之后,父节点才能进行自己的eval——典型的后序遍历。 
(话说,上图中节点左下角有蓝色标记的说明那是节点的“内在属性”。从属性语法的角度看,如果一个节点的某个属性的值只依赖于自身或子节点,则该属性被称为“综合属性”(synthesized attribute);如果一个节点的某个属性只依赖于自身、父节点和兄弟节点,则该属性被称为“继承属性”(inherited attribute)。上图中节点右下角的红色标记都只依赖子节点来计算,显然是综合属性。) 

SquirrelFish之前的JavaScriptCore、CRuby 1.9之前的CRuby就都是采用这种方式来解释执行的。 

可能需要说明的: 
·左值与右值 
在源代码i = a + b * c中,赋值符号左侧的i是一个标识符,表示一个变量,取的是变量的“左值”(也就是与变量i绑定的存储单元);右侧的a、b、c虽然也是变量,但取的是它们的右值(也就是与变量绑定的存储单元内的值)。在许多编程语言中,左值与右值在语法上没有区别,它们实质的差异容易被忽视。一般来说左值可以作为右值使用,反之则不一定。例如数字1,它自身有值就是1,可以作为右值使用;但它没有与可赋值的存储单元相绑定,所以无法作为左值使用。 
左值不一定只是简单的变量,还可以是数组元素或者结构体的域之类,可能由复杂的表达式所描述。因此左值也是需要计算的。 

·优先级、结合性与求值顺序 
这三个是不同的概念,却经常被混淆。通过AST来看就很容易理解:(假设源码是从左到右输入的) 
所谓优先级,就是不同操作相邻出现时,AST节点与根的距离的关系。优先级高的操作会更远离根,优先级低的操作会更接近根。为什么?因为整棵AST是以后序遍历求值的,显然节点离根越远就越早被求值。 
所谓结合性,就是当同类操作相邻出现时,操作的先后顺序同AST节点与根的距离的关系。如果是左结合,则先出现的操作对应的AST节点比后出现的操作的节点离根更远;换句话说,先出现的节点会是后出现节点的子节点。 
所谓求值顺序,就是在遍历子节点时的顺序。对二元运算对应的节点来说,先遍历左子节点再遍历右子节点就是从左到右的求值顺序,反之则是从右到左的求值顺序。 
这三个概念与运算的联系都很紧密,但实际描述的是不同的关系。前两者是解析器根据语法生成AST时就已经决定好的,后者则是解释执行或者生成代码而去遍历AST时决定的。 
在没有副作用的环境中,给定优先级与结合性,则无论求值顺序是怎样的都能得到同样的结果;而在有副作用的环境中,求值顺序会影响结果。 

赋值运算虽然是右结合的,但仍然可以用从左到右的求值顺序;事实上Java、C#等许多语言都在规范里写明表达式的求值顺序是从左到右的。上面的例子中就先遍历的=的左侧,求得i的左值;再遍历=的右侧,得到表达式的值23;最后执行=自身,完成对i的赋值。 
所以如果你要问:赋值在类似C的语言里明明是右结合的运算,为什么你先遍历左子树再遍历右子树?上面的说明应该能让你发现你把结合性与求值顺序混为一谈了。 

看看Java从左到右求值顺序的例子: 
Java代码  
  1. public class EvalOrderDemo {  
  2.     public static void main(String[] args) {  
  3.         int[] arr = new int[1];  
  4.         int a = 1;  
  5.         int b = 2;  
  6.         arr[0] = a + b;  
  7.     }  
  8. }  

由javac编译,得到arr[0] = a + b对应的字节码是: 
Java bytecode代码  
  1. // 左子树:数组下标  
  2. // a[0]  
  3. aload_1  
  4. iconst_0  
  5.   
  6. // 右子树:加法  
  7. // a  
  8. iload_2  
  9. // b  
  10. iload_3  
  11. // +  
  12. iadd  
  13.   
  14. // 根节点:赋值  
  15. iastore  



6、从树遍历解释器进化为基于栈的字节码解释器的前端 

如果你看到树形结构与后序遍历,并且知道后缀记法(或者逆波兰记法,reverse Polish notation)的话,那敏锐的你或许已经察觉了:要解释执行AST,可以先通过后序遍历AST生成对应的后缀记法的操作序列,然后再解释执行该操作序列。这样就把树形结构压扁,成为了线性结构。 
树遍历解释器对AST的求值其实隐式依赖于调用栈:eval(Node n)的递归调用关系是靠调用栈来维护的。后缀表达式的求值则通常显式依赖于一个栈,在遇到操作数时将其压入栈中,遇到运算时将合适数量的值从栈顶弹出进行运算,再将结果压回到栈上。这种描述看起来眼熟么?没错,后缀记法的求值中的核心数据结构就是前文提到过的“求值栈”(或者叫操作数栈,现在应该更好理解了)。后缀记法也就与基于栈的架构联系了起来:后者可以很方便的执行前者。同理,零地址指令也与树形结构联系了起来:可以通过一个栈方便的把零地址指令序列再转换回到树的形式。 

Java字节码与Java源码联系紧密,前者可以看成后者的后缀记法。如果想在JVM上开发一种语义能直接映射到Java上的语言,那么编译器很好写:秘诀就是后序遍历AST。 
那么让我们再来看看,同样是i = a + b * c这段源码对应的AST,生成Java字节码的例子: 
 
(假设a、b、c、i分别被分配到局部变量区的slot 0到slot 3) 
能看出Java字节码与源码间的对应关系了么? 
一个Java编译器的输入是Java源代码,输出是含有Java字节码的.class文件。它里面主要包含扫描器与解析器,语义分析器(包括类型检查器/类型推导器等),代码生成器等几大部分。上图所展示的就是代码生成器的工作。对Java编译器来说,代码生成就到字节码的层次就结束了;而对native编译器来说,这里刚到生成中间表示的部分,接下去是优化与最终的代码生成。 

如果你对PythonCRuby 1.9之类有所了解,会发现它们的字节码跟Java字节码在“基于栈”的这一特征上非常相似。其实它们都是由“编译器+VM”构成的,概念上就像是Java编译器与JVM融为一体一般。 
从这点看,Java与Python和Ruby可以说是一条船上的。虽说内部具体实现的显著差异使得先进的JVM比简单的JVM快很多,而JVM又普遍比Python和Ruby快很多。 

当解释器中用于解释执行的中间代码是树形时,其中能被称为“编译器”的部分基本上就是解析器;中间代码是线性形式(如字节码)时,其中能被称为编译器的部分就包括上述的代码生成器部分,更接近于所谓“完整的编译器”;如果虚拟机是基于寄存器架构的,那么编译器里至少还得有虚拟寄存器分配器,又更接近“完整的编译器”了。 


7、基于栈与基于寄存器架构的VM的一组图解 

要是拿两个分别实现了基于栈与基于寄存器架构、但没有直接联系的VM来对比,效果或许不会太好。现在恰巧有两者有紧密联系的例子——JVM与Dalvik VM。JVM的字节码主要是零地址形式的,概念上说JVM是基于栈的架构。Google Android平台上的应用程序的主要开发语言是Java,通过其中的Dalvik VM来运行Java程序。为了能正确实现语义,Dalvik VM的许多设计都考虑到与JVM的兼容性;但它却采用了基于寄存器的架构,其字节码主要是二地址/三地址混合形式的,乍一看可能让人纳闷。考虑到Android有明确的目标:面向移动设备,特别是最初要对ARM提供良好的支持。ARM9有16个32位通用寄存器,Dalvik VM的架构也常用16个虚拟寄存器(一样多……没办法把虚拟寄存器全部直接映射到硬件寄存器上了);这样Dalvik VM就不用太顾虑可移植性的问题,优先考虑在ARM9上以高效的方式实现,发挥基于寄存器架构的优势。 
Dalvik VM的主要设计者Dan Bornstein在Google I/O 2008上做过一个关于Dalvik内部实现的演讲;同一演讲也在Google Developer Day 2008 China和Japan等会议上重复过。这个演讲中Dan特别提到了Dalvik VM与JVM在字节码设计上的区别,指出Dalvik VM的字节码可以用更少指令条数、更少内存访问次数来完成操作。(看不到YouTube的请自行想办法) 

眼见为实。要自己动手感受一下该例子,请先确保已经正确安装JDK 6,并从官网获取Android SDK 1.6R1。连不上官网的也请自己想办法。 

创建Demo.java文件,内容为: 
Java代码  
  1. public class Demo {  
  2.     public static void foo() {  
  3.         int a = 1;  
  4.         int b = 2;  
  5.         int c = (a + b) * 5;  
  6.     }  
  7. }  

通过javac编译,得到Demo.class。通过javap可以看到foo()方法的字节码是: 
Java bytecode代码  
  1. 0:  iconst_1  
  2. 1:  istore_0  
  3. 2:  iconst_2  
  4. 3:  istore_1  
  5. 4:  iload_0  
  6. 5:  iload_1  
  7. 6:  iadd  
  8. 7:  iconst_5  
  9. 8:  imul  
  10. 9:  istore_2  
  11. 10: return  


接着用Android SDK里platforms\android-1.6\tools目录中的dx工具将Demo.class转换为dex格式。转换时可以直接以文本形式dump出dex文件的内容。使用下面的命令: 
Command prompt代码  
  1. dx --dex --verbose --dump-to=Demo.dex.txt --dump-method=Demo.foo --verbose-dump Demo.class  

可以看到foo()方法的字节码是: 
Dalvik bytecode代码  
  1. 0000: const/4       v0, #int 1 // #1  
  2. 0001: const/4       v1, #int 2 // #2  
  3. 0002: add-int/2addr v0, v1  
  4. 0003: mul-int/lit8  v0, v0, #int 5 // #05  
  5. 0005: return-void  

(原本的输出里还有些code-address、local-snapshot等,那些不是字节码的部分,可以忽略。) 

让我们看看两个版本在概念上是如何工作的。 
JVM: 
 
(图中数字均以十六进制表示。其中字节码的一列表示的是字节码指令的实际数值,后面跟着的助记符则是其对应的文字形式。标记为红色的值是相对上一条指令的执行状态有所更新的值。下同) 
说明:Java字节码以1字节为单元。上面代码中有11条指令,每条都只占1单元,共11单元==11字节。 
程序计数器是用于记录程序当前执行的位置用的。对Java程序来说,每个线程都有自己的PC。PC以字节为单位记录当前运行位置里方法开头的偏移量。 
每个线程都有一个Java栈,用于记录Java方法调用的“活动记录”(activation record)。Java栈以帧(frame)为单位线程的运行状态,每调用一个方法就会分配一个新的栈帧压入Java栈上,每从一个方法返回则弹出并撤销相应的栈帧。 
每个栈帧包括局部变量区、求值栈(JVM规范中将其称为“操作数栈”)和其它一些信息。局部变量区用于存储方法的参数与局部变量,其中参数按源码中从左到右顺序保存在局部变量区开头的几个slot。求值栈用于保存求值的中间结果和调用别的方法的参数等。两者都以字长(32位的字)为单位,每个slot可以保存byte、short、char、int、float、reference和returnAddress等长度小于或等于32位的类型的数据;相邻两项可用于保存long和double类型的数据。每个方法所需要的局部变量区与求值栈大小都能够在编译时确定,并且记录在.class文件里。 
在上面的例子中,Demo.foo()方法所需要的局部变量区大小为3个slot,需要的求值栈大小为2个slot。Java源码的a、b、c分别被分配到局部变量区的slot 0、slot 1和slot 2。可以观察到Java字节码是如何指示JVM将数据压入或弹出栈,以及数据是如何在栈与局部变量区之前流动的;可以看到数据移动的次数特别多。动画里可能不太明显,iadd和imul指令都是要从求值栈弹出两个值运算,再把结果压回到栈上的;光这样一条指令就有3次概念上的数据移动了。 

对了,想提醒一下:Java的局部变量区并不需要把某个局部变量固定分配在某个slot里;不仅如此,在一个方法内某个slot甚至可能保存不同类型的数据。如何分配slot是编译器的自由。从类型安全的角度看,只要对某个slot的一次load的类型与最近一次对它的store的类型匹配,JVM的字节码校验器就不会抱怨。以后再找时间写写这方面。 

Dalvik VM: 
 
说明:Dalvik字节码以16位为单元(或许叫“双字节码”更准确 =_=|||)。上面代码中有5条指令,其中mul-int/lit8指令占2单元,其余每条都只占1单元,共6单元==12字节。 
与JVM相似,在Dalvik VM中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。PC记录的是以16位为单位的偏移量而不是以字节为单位的。 
与JVM不同的是,Dalvik VM的栈帧中没有局部变量区与求值栈,取而代之的是一组虚拟寄存器。每个方法被调用时都会得到自己的一组虚拟寄存器。常用v0-v15这16个,也有少数指令可以访问v0-v255范围内的256个虚拟寄存器。与JVM相同的是,每个方法所需要的虚拟寄存器个数都能够在编译时确定,并且记录在.dex文件里;每个寄存器都是字长(32位),相邻的一对寄存器可用于保存64位数据。方法的参数按源码中从左到右的顺序保存在末尾的几个虚拟寄存器里。 
与JVM版相比,可以发现Dalvik版程序的指令数明显减少了,数据移动次数也明显减少了,用于保存临时结果的存储单元也减少了。 

你可能会抱怨:上面两个版本的代码明明不对应:JVM版到return前完好持有a、b、c三个变量的值;而Dalvik版到return-void前只持有b与c的值(分别位于v0与v1),a的值被刷掉了。 
但注意到a与b的特征:它们都只在声明时接受过一次赋值,赋值的源是常量。这样就可以对它们应用常量传播,将 
Java代码  
  1. int c = (a + b) * 5;  

替换为 
Java代码  
  1. int c = (1 + 2) * 5;  

然后可以再对c的初始化表达式应用常量折叠,进一步替换为: 
Java代码  
  1. int c = 15;  

把变量的每次状态更新(包括初始赋值在内)称为变量的一次“定义”(definition),把每次访问变量(从变量读取值)称为变量的一次“使用”(use),则可以把代码整理为“使用-定义链”(简称UD链,use-define chain)。显然,一个变量的某次定义要被使用过才有意义。上面的例子经过常量传播与折叠后,我们可以分析得知变量a、b、c都只被定义而没有被使用。于是它们的定义就成为了无用代码(dead code),可以安全的被消除。 
上面一段的分析用一句话描述就是:由于foo()里没有产生外部可见的副作用,所以foo()的整个方法体都可以被优化为空。经过dx工具处理后,Dalvik版程序相对JVM版确实是稍微优化了一些,不过没有影响程序的语义,程序的正确性是没问题的。这是其一。 

其二是Dalvik版代码只要多分配一个虚拟寄存器就能在return-void前同时持有a、b、c三个变量的值,指令几乎没有变化: 
Dalvik bytecode代码  
  1. 0000: const/4      v0, #int 1 // #1  
  2. 0001: const/4      v1, #int 2 // #2  
  3. 0002: add-int      v2, v0, v1  
  4. 0004: mul-int/lit8 v2, v2, #int 5 // #05  
  5. 0006: return-void  

这样比原先的版本多使用了一个虚拟寄存器,指令方面也多用了一个单元(add-int指令占2单元);但指令的条数没变,仍然是5条,数据移动的次数也没变。 

题外话1:Dalvik VM是基于寄存器的,x86也是基于寄存器的,但两者的“寄存器”却相当不同:前者的寄存器是每个方法被调用时都有自己一组私有的,后者的寄存器则是全局的。也就是说,概念上Dalvik VM字节码中不用担心保护寄存器的问题,某个方法在调用了别的方法返回过来后自己的寄存器的值肯定跟调用前一样。而x86程序在调用函数时要考虑清楚calling convention,调用方在调用前要不要保护某些寄存器的当前状态,还是说被调用方会处理好这些问题,麻烦事不少。Dalvik VM这种虚拟寄存器让人想起一些实际处理器的“寄存器窗口”,例如SPARC的Register Windows也是保证每个函数都觉得自己有“私有的一组寄存器”,减轻了在代码里处理寄存器保护的麻烦——扔给硬件和操作系统解决了。IA-64也有寄存器窗口的概念。 
(当然,Dalvik VM与x86的“寄存器”一个是虚拟寄存器一个是真实硬件的ISA提供的寄存器,本来也不在一个级别上…上面这段只是讨论寄存器的语义。) 

题外话2:Dalvik的.dex文件在未压缩状态下的体积通常比同等内容的.jar文件在deflate压缩后还要小。但光从字节码看,Java字节码几乎总是比Dalvik的小,那.dex文件的体积是从哪里来减出来的呢?这主要得益与.dex文件对常量池的压缩,一个.dex文件中所有类都共享常量池,使得相同的字符串、相同的数字常量等都只出现一次,自然能大大减小体积。相比之下,.jar文件中每个类都持有自己的常量池,诸如"Ljava/lang/Object;"这种常见的字符串会被重复多次。Sun自己也有进一步压缩JAR的工具,Pack200,对应的标准是JSR 200。它的主要应用场景是作为JAR的网络传输格式,以更高的压缩比来减少文件传输时间。在官方文档提到了Pack200所用到的压缩技巧, 
JDK 5.0 Documentation 写道
Pack200 works most efficiently on Java class files. It uses several techniques to efficiently reduce the size of JAR files: 
可见.dex文件与Pack200采用了一些相似的减小体积的方法。很可惜目前还没有正式发布的JVM支持直接加载Pack200格式的归档,毕竟网络传输才是Pack200最初构想的应用场景。 

再次提醒注意,上面的描述是针对概念上的JVM与Dalvik VM,而不是针对它们的具体实现。实现VM时可以采用许多优化技巧去减少性能损失,使得实际的运行方式与概念中的不完全相符,只要最终的运行结果满足原本概念上的VM所实现的语义就行。 

=========================================================================== 

上面“简单”的提了些讨论点,不过还没具体到JavaScript引擎,抱歉。弄得太长了,只好在这里先拆分一次……有些东西想写的,洗个澡又忘记了。等想起来再补充 orz 
“简单”是相对于实际应该掌握的信息量而言。上面写的都还没挠上痒痒,心虚。 
Anyway。根据拆分的现状,下一篇应该是讨论动态语言与编译的问题,然后再下一篇会看看解释器的演化方法,再接着会看看JavaScript引擎的状况(主要针对V8和Nitro,也会谈谈Tamarin。就不讨论JScript了)。 

关于推荐资料,在“我的收藏”的virtual machine标签里就有不少值得一读的资料。如果只是对JavaScript引擎相关感兴趣的话也可以选着读些。我的收藏里还有v8和tamarin等标签的,资料有的是 ^ ^ 

能有耐心读到结尾的同学们,欢迎提出意见和建议,以及指出文中的错漏 ^_^ 
不像抓到虫就给美分的大师,我没那种信心……错漏难免,我也需要进一步学习。拜托大家了~ 

P.S. 画图真的很辛苦,加上JavaEye的带宽也不是无限的……所以拜托不要直接链接这帖里的图 <(_ _)> 
有需要原始图片的可以跟我联系。我是画成多帧PNG然后转换为GIF发出来的。上面的PNG图片都还保留有原始的图层信息,要拿去再编辑也很方便 ^ ^ 

更新1: 
原本在树遍历解释器图解的小节中,我用的是这幅图: 
 
其实上图画得不准确,a、b、c的右值不应该画在节点上的;节点应该只保存了它们的左值才对,要获取对应的右值就要查询变量表。我修改了图更新到正文了。原本的图里对i的赋值看起来很奇怪,就像是遍历过程经过了两次i节点一般,而事实不是那样的。

1 什么是Dalvik虚拟机

Dalvik是Google公司自己设计用于Android平台的Java虚拟机,它是Android平台的重要组成部分,支持dex格式(Dalvik Executable)的Java应用程序的运行。dex格式是专门为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Google对其进行了特定的优化,使得Dalvik具有高效、简洁、节省资源的特点。从Android系统架构图知,Dalvik虚拟机运行在Android的运行时库层。

2 Dalvik虚拟机的功能

Dalvik作为面向Linux、为嵌入式操作系统设计的虚拟机,主要负责完成对象生命周期管理、堆栈管理、线程管理、安全和异常管理,以及垃圾回收等。Dalvik充分利用Linux进程管理的特定,对其进行了面向对象的设计,使得可以同时运行多个进程,而传统的Java程序通常只能运行一个进程,这也是为什么Android不采用JVM的原因。Dalvik为了达到优化的目的,底层的操作大多和系统内核相关,或者直接调用内核接口。另外,Dalvik早期并没有JIT编译器,直到Android2.2才加入了对JIT的技术支持。

3 Dalvik虚拟机和Java虚拟机的区别

本质上,Dalvik也是一个Java虚拟机。但它特别之处在于没有使用JVM规范。大多数Java虚拟机都是基于栈的结构(详情请参考:理解Java虚拟机体系结构),而Dalvik虚拟机则是基于寄存器。基于栈的指令很紧凑,例如,Java虚拟机使用的指令只占一个字节,因而称为字节码。基于寄存器的指令由于需要指定源地址和目标地址,因此需要占用更多的指令空间。Dalvik虚拟机的某些指令需要占用两个字节。基于栈和基于寄存器的指令集各有优劣,一般而言,执行同样的功能,前者需要更多的指令(主要是load和store指令),而后者需要更多的指令空间。需要更多指令意味着要多占用CPU时间,而需要更多指令空间意味着数据缓冲(d-cache)更易失效。更多讨论,虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩 给出了非常详细的参考。
Java虚拟机运行的是Java字节码,而Dalvik虚拟机运行的是专有文件格式dex。在Java程序中,Java类会被编译成一个或多个class文件,然后打包到jar文件中,接着Java虚拟机会从相应的class文件和jar文件中获取对应的字节码。Android应用虽然也使用Java语言,但是在编译成class文件后,还会通过DEX工具将所有的class文件转换成一个dex文件,Dalvik虚拟机再从中读取指令和数据。dex文件除了减少整体的文件尺寸和I/O操作次数,也提高了类的查找速度。
由下图可以看到,jar和apk文件的组成结构,以及class文件和dex文件的差异。dex格式文件使用共享的、特定类型的常量池机制来节省内存。常量池存储类中的所有字面常量,它包括字符串常量、字段常量等值。
总的来说,Dalvik虚拟机具有以下特点:

4 Dalvik系统结构

实际上,Dalvik是基于Apache Harmony(Apache软件基金会的Java SE项目)的部分实现,提供了自己的一套库,即上层Java应用程序编写所使用的API。
以上图示来自tech-insider。Apache Harmony大体上分为三个层:操作系统、Java虚拟机、Java类库。它的特点在于虚拟机和类库内部被高度模块化,每一个模块都有一定的接口定义。操作系统层与虚拟机层之间的接口由Portability Layer定义,它封装了不同操作系统的差异,为虚拟机和类库的本地代码提供了一套统一的API访问底层系统调用。虚拟机与类库之间的接口除了Java规范定义的JNI、JVMITI外,还加入了一层虚拟机接口,由内核类和本地代码组成。实现了虚拟机接口的虚拟机都可以使用Harmony的类库实现,并且可以被Harmony提供的同一个Java启动程序启动。
下面是Dalvik虚拟机的结构图:
一个应用首先经过DX工具将class文件转换成Dalvik虚拟机可以执行的dex文件,然后由类加载器加载原生类和Java类,接着由解释器根据指令集对Dalvik字节码进行解释、执行。最后,根据dvm_arch参数选择编译的目标机体系结构。

4.1 dex文件结构

dex文件结构和class文件结构差异的地方很多,但从携带的信息上看,dex和class文件是一致的。
更多dex格式的内容,Android安全–Dex文件格式详解 这篇文章进行了非常详细的介绍。虽然dex文件的结构很紧凑,但想要运行时的性能得到进一步提升,还需要对dex文件进行进一步优化。优化主要针对以下几个方面:
dex文件经过优化后文件大小会膨胀,大约增加到原来的1~4倍。对于内置应用,一般在系统编译后,便会生成优化文件(odex: Optimized dex)。一个Android应用程序,需要经过以下过程才可以在Dalvik虚拟机上运行:
上图(来自网络)详尽地展示了最终签名后的APK是怎么来的。

4.2 Dalvik类加载器

一个dex文件需要类加载器加载原生类和Java类,然后通过解释器根据指令集对Dalvik字节码进行解释和执行。Dalvik类加载器使用mmap函数,将dex文件映射到内存中,通过普通的内存读取操作即可访问dex文件,然后解析dex文件内容并加载其中的类到哈希表中。

4.2.1 解析dex

总的来说,dex文件可以抽象为三个部分:头部、索引、数据。通过头部可以知道索引的位置和数目,以及数据区的起始位置。将dex文件映射到内存后,Dalvik会调用dexFileParse函数对其进行分析,分析的结果放到DexFile数据结构中。DexFile中的baseAddr指向映射区的起始位置,pClassDefs指向class索引的起始位置。为了加快class的查找速度,还创建一个哈希表,对class名字进行哈希并生成索引。

4.2.2 加载class

解析工作完成后就进行class的加载,加载的类需要用ClassObject数据结构来存储。
1
2
3
4
typedef struct Object {
    ClassObject* clazz;  // 类型对象
    Lock lock;           // 锁对象
} Object;
其中clazz指向ClassObject对象,还包含一个Lock对象。如果其它线程想要获取它的锁,只有等这个线程释放。Dalvik每加载一个class都会对应一个ClassObject对象,加载过程会在内存中分配几个区域,分别存放directMethod, virtualMethod, sfield, ifield。这些信息从dex文件的数据区中读取。字段Field的定义如下:
1
2
3
4
5
6
7
8
9
10
11
struct Field {
    ClassObject* clazz;    //所属类型
    const char* name;      // 变量名称
    const char* signature; // 如“Landroid/os/Debug;”
    u4 accessFlags;        // 访问标记
 
    #ifdef PROFILE_FIELD_ACCESS
        u4 gets;
        u4 puts;
    #endif
};
待得到class索引后,实际的加载由loadClassFromDex来完成。首先它会读取class的具体数据,分别加载directMethod, virtualMethod, ifield和sfield,然后为ClassObject数据结构分配内存,并读取dex文件的相关信息。加载完成后,将加载的class通过dvmAddClassToHash函数放入哈希表,以方便下次查找;最后,通过dvmLinkClass查找该类的超类,如果有接口类则加载相应的接口类。

4.3 Dalvik解释器

对于任何虚拟机来说,解释器无疑是核心的部分,所有的Java字节码都经过解释器解释执行。由于Dalvik解释器的效率很重要,Android分别实现了C语言版和各种汇编语言版的解释器。解释器通常是循环执行,需要一个入口函数调用处理程序执行第一条指令,而后每条指令执行时引出下一条指令,通过函数指针调用处理程序。

4.4 内存管理

垃圾收集是Dalvik虚拟机内存管理的核心。此处只介绍Dalvik虚拟机的垃圾收集功能。垃圾收集的性能在很大程度上影响了一个Java程序内存使用的效率。Dalvik虚拟机使用常用的Mark-Sweep算法,该算法分Mark阶段(标记出活动对象)、Sweep阶段(回收垃圾内存)和可选的Compact阶段(减少堆中的碎片)。Android内存管理原理  这篇文章讲解得很详细。
垃圾收集的第一步是标记出活动对象,因为没有办法识别那些不可访问的对象,这样所有未被标记的对象就是可以回收的垃圾。当进行垃圾收集时,需要停止Dalvik虚拟机的运行(除垃圾收集外),因此垃圾收集又被称作STW(stop-the-world)。Dalvik虚拟机在运行过程中要维护一些状态信息,这些信息包括:每个线程所保存的寄存器、Java类中的静态字段、局部和全局的JNI引用,JVM中的所有函数调用会对应一个相应C的栈帧。每一个栈帧里可能包含对对象的引用,比如包含对象引用的局部变量和参数。所有这些引用信息被加入到一个根集合中,然后从根集合开始,递归查找可以从根集合出发访问的对象。因此,Mark过程又叫做追踪,追踪所有可被访问的对象。
垃圾收集的第二步就是回收内存。在Mark阶段通过markBits位图可以得到所有可访问的对象集合,而liveBits位图表示所有已经分配的对象集合。通过比较liveBits位图和markBits位图的差异就是所有可回收的对象集合。Sweep阶段调用free来释放这些内存给堆。
在底层内存实现上,Android系统使用的是msspace,这是一个轻量级的malloc实现。除了创建和初始化用于存储普通Java对象的内存堆,Android还创建三个额外的内存堆:
虚拟机通过一个名为gHs的全局HeapSource变量来操控GC内存堆,而HeapSource里通过heaps数组可以管理多个堆(Heap),以满足动态调整GC内存堆大小的要求。另外HeapSource里还维护一个名为”livebits”的位图索引,以跟踪各个堆(Heap)的内存使用情况。剩下两个数据结构”markstack”和”markbits”都是用在垃圾回收阶段。
上图中”livebits”维护堆上已用的内存信息,而”markbits”这个位图索引则指向存活的对象。 A、C、F、G、H对象需要保留,因此”markbits”分别指向他们(最后的H对象尚在标注过程中,因此没有指针指向它)。而”markstack”就是在标注过程中跟踪当前需要处理的对象要用到的标志栈,此时其保存了正在处理的对象F、G和H。

4.5 Dalvik的启动流程

Dalvik进程管理是依赖于linux的进程体系结构的,如要为应用程序创建一个进程,它会使用linux的fork机制来复制一个进程。Zygote是一个虚拟机进程,同时也是一个虚拟机实例的孵化器,它通过init进程启动。之前的文章有对此过程有详细介绍:Android系统启动分析(Init->Zygote->SystemServer->Home activity)。此处分析Dalvik虚拟机启动的相关过程。
AndroidRuntime类主要做了以下几件事情:
在JNI中,dvmCreateJNIEnv为当前线程创建和初始化一个JNI环境,即一个JNIEnvExt对象。最后调用dvmStartup来初始化前面创建的Dalvik虚拟机实例。函数dvmInitZygote调用了系统的setpgid来设置当前进程,即Zygote进程的进程组ID。这一步完成后,Dalvik虚拟机的创建和初始化工作就完成了。

5 Android的启动

 
 
参考:
《Android技术内幕》
Dalvik虚拟机简要介绍和学习计划
深入理解Android(二):Java虚拟机Dalvik
dalvik虚拟内存管理之二——垃圾收集
Dalvik虚拟机的启动过程分析

JVM(Java 虚拟机)目前主要是这4个. 微软JVM死了N多年了,GCJ支持到1.4.2之后,处于停滞状态.现在市面上能见到的,也就是这4种. 
Sun JVM:
Sun公司的产品,也是平时用得最多的.一提到JVM,很多人自然就想到SUN的Java虚拟机.其实不然,Java现在已经可以看成一种"事实上"的标准.就和C,C++一样.Sun的虚拟机特点:中规中矩,最标准,应用平台最多. 
JRocket:
JRocket是BEA公司的JVM,号称世界上最快的JVM.这款虚拟机其实也很常见.使用WebLogic的用户,往往使用JRocket虚拟机. 因为其内存回收算法的特别,理论上据说使得JRocket在Windows平台上的表现要比Sun Java好一些.但是偶的感觉是,在Linux下,JRocket表现得没有Sun Java稳定(有时会莫名其妙down掉).至于JRocket号称的快速,我倒从没感觉出来过. 这款虚拟机是商业产品,但是可以免费使用.它自带的"JRocket 控制台",做得很不错.
BEA已经被Oracle收购了,所以准确地说,现在是Oracle的产品 
J9:
IBM产的JVM,也号称过世界上最快的Java虚拟机. 我没感受过它的快,只觉得这个玩意儿超级不稳定(IBM软件的通病). 和Websphere 应用服务器配合使用,倒是很少出差错.但是换了普通Java程序,就10有八九出问题了.
也有人告诉我,这个虚拟机跑在AIX大机上是很稳定的,Windows下根本不能用,也许吧.IBM做的软件,永远是那德行. 
Harmony
IBM和Intel搞的开源JVM. IBM牵头,不过主力是Intel.为此Intel去年在国内还大张旗鼓招聘Java程序员.C部分的代码不消说,就算是Java部分的代码,也是Intel在努力搞. 这玩意儿纯粹是IBM-Intel和SUN扯皮的结果.想借此获得Java世界的话语权. IBM利用Harmony来攻击Sun的不开源,结果Sun把Java在GPL协议下开源之后,IBM又攻击SUN把Java "不负责任地开源"---只有Harmony用Apache License协议才是合适的.Sun也借此反击,坚决不让Harmony获得JCP认证.....口水战满天飞,苦的是Intel.目前版本是5.0M4,正式版遥遥无期. (Harmony着实要比J9好,至少兼容性要好多!)
墙里开花墙外香,Harmony在Java时间里一直没取得名分,独守空房地时候,被Google看中,作为其手机操作系统Android里的虚拟机.
由此,Java世界里第二次事实上的分裂终于出现(j++到.NET的发展,偶觉得可以看做是第一次分裂).
Harmony在Android里修成正果.虽然语法是99%的Java语法,但是字节码结构,连接模型..这些JCP标准里定义的东西,已经完全对不上号.这就好比我们写着J#.NET---即便语法是Java的.

AIX,是IBM专有UNIX操作系统的商标名。名称来自先进交互运行系统英语:Advanced Interactive executive,缩写为)。最初的名称来自英语:Advanced IBM Unix,但或许这个名字没有得到法律部门的允许,因此更改为"Advanced Interactive eXecutive"。
AIX的一些流行特性例如chuser、mkuser、rmuser命令以及相似的东西允许如同管理文件一样来进行用户管理。AIX级别的逻辑卷管理正逐渐被添加进各种自由UNIX风格操作系统中。
AIX 5L 5.3版本操作系统可以支持:
目录  [隐藏

专用文件系统[编辑]

IBM最早在1990年2月于AIX 3.1引入初始版本的JFS。这个版本的JFS现在被叫作JFS1, 是AIX在往后十多年的首选档案系统并被安装在过百万台IBM顾客的AIX系统中。JFS1和AIX的内存管理程序紧紧连结在一起[6],这种设计经常在一些封闭源码作业系统或只支持一个作业系统的档案系统出现。
1995年,强化JFS的工作开始展开,当中包括加强其伸延性,支持多微处理器的计算机和令其易于移植至其他作业系统。经过多年的设计、改良和测试,新的JFS在1999年4月付运于OS/2 Warp Server for eBusiness,随后亦付运在2000年10月的OS/2 Warp Client中。与此同时,,JFS开发团亦在1997年开始把开发中新版JFS移植回AIX。为和原身AIX支持的原版JFS1分开,新版JFS亦会称作JFS2 (Enhanced Journaled File System)。2001年5月,JFS2开正式可供AIX 5L使用。
1999年10月,原供OS/2并正在移植回AIX的新版JFS源码被以GNU General Public License开放给自由/开放原始码软件社区并展开了移植至Linux的工作。而第一个稳定版本的JFS for Linux亦在2001年6月推出。[7]至2002年8月,JFS正式并入稳定版Linux核心2.4.20。[8]
AIX操作系统使用JFS文件系统(JOURNAL FILE SYSTEM), JFS文件系统是一种字节级日志文件系统,借鉴了数据库保护系统的技术,以日志的形式记录文件的变化。JFS通过记录文件结构而不是数据本身的变化来保证数据的完整性。这种方式可以确保在任何时刻都能维护数据的可访问性。
该文件系统主要是为满足服务器(从单处理器系统到高级多处理器和群集系统)的高吞吐量和可靠性需求而设计、开发的。JFS文件系统是为面向事务的高性能系统而开发的。在IBM的AIX系统上,JFS已经过较长时间的测试,结果表明它是可靠、快速和容易使用的。JFS也是一个有大量用户安装使用的企业级文件系统,具有可伸缩性和健壮性。与非日志文件系统相比,它的突出优点是快速重启能力,JFS能够在几秒或几分钟内就把文件系统恢复到一致状态。虽然JFS主要是为满足服务器(从单处理器系统到高级多处理器和群集系统)的高吞吐量和可靠性需求而设计的,但还可以用于想得到高性能和可靠性的客户机配置,因为在系统崩溃时JFS能提供快速文件系统重启时间,所以它是因特网文件服务器的关键技术。使用数据库日志处理技术,JFS能在几秒或几分钟之内把文件系统恢复到一致状态。而在非日志文件系统中,文件恢复可能花费几小时或几天。
JFS的缺点是,使用JFS日志文件系统性能上会有一定损失,系统资源占用的比率也偏高,因为当它保存一个日志时,系统需要写许多数据。
JFS2(Enhanced Journaled File System),通常,这个新的文件系统被称为JFS2,从2001年5月开始,JFS2正式可以在AIX 5L上使用
JFS2支持预定的日志记录方式,可以提高较高的性能,并实现亚秒级文件系统恢复。JFS2同时为提高性能提供了基于分区的文件分配(Extent-based allocation)。基于分区的分配 是指对一组连续的块而非单一的块进行分配。由于这些块在磁盘上是连续的,其读取和写入的性能就会更好。这种分配的另外一个优势就是可以将元数据管理最小化。按块分配磁盘空间就意味着要逐块更新元数据。而使用分区,元数据则仅需按照分区(可以代表多个块)更新。JFS2还使用了B+ 树,以便更快地查找目录和管理分区描述符。JFS2没有内部日志提交策略,而是在kupdate守护进程超时时提交。
jfs和jfs2文件系统都是文件和目录的集合,管理文件或目录在磁盘上的位置。除了文件和目录以外,jfs2类型的文件系统还包含一个超级块、分配位图和一个或多个分配组。分配组由磁盘i节点和片区(Extent)组成。一个jfs2类型的文件系统也占据一个逻辑卷。
在jfs中,超级块的大小是4096字节,偏移量是4096字节;而在jfs2中,超级块的大小仍是4096字节,但是超级块在逻辑卷中的偏移量是32768字节。
jfs2的新功能包括基于片区的(Extent)的分配、目录排序和文件系统对象的动态空间分配等。
1.基于片区(Extent)的寻址结构
片区是一个连续的可变长的文件系统块的序列,它是jfs2对象的分配单位。"大片区"可以跨越多个分配组。一般而言,jfs2的分配策略通过为每一个片区分配尽可能大和连续的区间来使文件系统中片区的数量达到最小,使逻辑块的分配更加连续。这样能够提供更大的i/o传输效率,达以改善性能的目的。但是在有些情况上并不能总是达到这种理想的效果。
片区是由逻辑块偏移量(logical offset)、长度(length)和物理地址(physical address)组成的三元组来描述。其中由逻辑块偏移量和长度可能计算出物理地址。基于片区的寻址结构是由片区描述、作为根的i节点和作为键值的文件内的逻辑偏移量而构成的一个子B+树。
2.可变的逻辑块大小
JFS2把磁盘空间分区成块,JFS2支持512,1024,2048和4096字节块的大小。不同的文件系统可以使用不同的块的大小,从而达到优化空间的目的,减少目录或文件内部的残片(Fragmentation).
3.动态分配磁盘i节点
JFS2给磁盘i节点动态地按需分配空间,当i节点不再需要时就会释放i节点所占用的空间。这个特点避免了在创建标准JFS时为磁盘i节点预留固定数量磁盘空间的缺点。因此,这样就不需要用户在创建文件系统时计算这个文件系统中要保存的文件和目录的最大数。
4.目录组织   JFS2提供了两种不同的目录组织.第1种目录组织适用于小目录和在一个目录的i节点中保存目录的内容.这种目录组织不需要单独的目录块i/o和单独的存储分配. 第2种目录组织适用于较大的目录,每一个目录就是一个以名字为键值的B+树.与传统的、线性的、未分级的目录组织相比,这种目录组织能够提供更快的目录查找、插入和删除的能力。
5.在线整理文件系统的空闲残片
JFS2支持已安装的文件系统(即使有进程访问这个文件系统)对残余在文件系统中的空闲空间的整理功能。一旦一个文件系统的空闲空间变成分散的残片,对这些残片的整理将会使得JFS2提供I/O效率更高的磁盘空间分配,从而避免出现一些因空闲空间不连续而不够分配的情况。

发布历史[编辑]

一些不同版本的AIX曾经存在过,但是逐渐消失了。1986年出现的AIX V1运行在IBM RT/PC(AIX/RT)上。它基于System V Release 3。自从1989年以来,AIX成为RS/6000系列工作站和服务器(AIX/6000)的操作系统。在AIX的开发过程中,IBM和INTERACTIVE Systems Corporation(同IBM签约)将4.2BSD与4.3BSD的一些特性加入了AIX中。值得一提的是,著名的深蓝的操作系统即是AIX

支持的架构[编辑]

版本[编辑]

注:L表示同Linux的姻缘关系

图形界面[编辑]

通用桌面环境(Common Desktop Environment,CDE)是AIX系统的默认图形用户界面。作为同Linux联姻的一部分,针对Linux应用的AIX工具箱(ATLA)也提供了开源的KDEGNOME桌面。

外部链接[编辑]



转载请标明出处: 
http://blog.csdn.net/forezp/article/details/70148833 
本文出自方志朋的博客
错过了这一篇,你可能再也学不会 Spring Cloud 了!Spring Boot做为下一代 web 框架,Spring Cloud 作为最新最火的微服务的翘楚,你还有什么理由拒绝。赶快上船吧,老船长带你飞。终章不是最后一篇,它是一个汇总,未来还会写很多篇。
案例全部采用Spring Boot 1.5.x ,Spring Cloud版本为Dalston.RELEASE
我为什么这些文章?一是巩固自己的知识,二是希望有更加开放和与人分享的心态,三是接受各位大神的批评指教,有任何问题可以联系我: miles02@163.com
码农下载:https://git.oschina.net/forezp/SpringCloudLearning 
github下载:https://github.com/forezp/SpringCloudLearning,记得star哦!

CSDN专栏汇总:史上最简单的 SpringCloud 教程

《史上最简单的 SpringCloud 教程》系列:

进阶篇

源码篇:

番外篇:



一个Eclipse骨灰级开发者总结了他认为最有用但又不太为人所知的快捷键组合。通过这些组合可以更加容易的浏览源代码,使得整体的开发效率和质量得到提升。
    1. ctrl+shift+r:打开资源
    这可能是所有快捷键组合中最省时间的了。这组快捷键可以让你打开你的工作区中任何一个文件,而你只需要按下文件名或mask名中的前几个字母,比如applic*.xml。美中不足的是这组快捷键并非在所有视图下都能用。

2. ctrl+o:快速outline
    如果想要查看当前类的方法或某个特定方法,但又不想把代码拉上拉下,也不想使用查找功能的话,就用ctrl+o吧。它可以列出当前类中的所有方法及属性,你只需输入你想要查询的方法名,点击enter就能够直接跳转至你想去的位置。

3. ctrl+e:快速转换编辑器
    这组快捷键将帮助你在打开的编辑器之间浏览。使用ctrl+page down或ctrl+page up可以浏览前后的选项卡,但是在很多文件打开的状态下,ctrl+e会更加有效率。

4. ctrl+2,L:为本地变量赋值
    开发过程中,我常常先编写方法,如Calendar.getInstance(),然后通过ctrl+2快捷键将方法的计算结果赋值于一个本地变量之上。 这样我节省了输入类名,变量名以及导入声明的时间。Ctrl+F的效果类似,不过效果是把方法的计算结果赋值于类中的域。
    5. alt+shift+r:重命名
    重命名属性及方法在几年前还是个很麻烦的事,需要大量使用搜索及替换,以至于代码变得零零散散的。今天的Java IDE提供源码处理功能,Eclipse也是一样。现在,变量和方法的重命名变得十分简单,你会习惯于在每次出现更好替代名称的时候都做一次重命名。要使 用这个功能,将鼠标移动至属性名或方法名上,按下alt+shift+r,输入新名称并点击回车。就此完成。如果你重命名的是类中的一个属性,你可以点击alt+shift+r两次,这会呼叫出源码处理对话框,可以实现get及set方法的自动重命名。
    6. alt+shift+l以及alt+shift+m:提取本地变量及方法
    源码处理还包括从大块的代码中提取变量和方法的功能。比如,要从一个string创建一个常量,那么就选定文本并按下alt+shift+l即可。如果同 一个string在同一类中的别处出现,它会被自动替换。方法提取也是个非常方便的功能。将大方法分解成较小的、充分定义的方法会极大的减少复杂度,并提 升代码的可测试性。
    7. shift+enter及ctrl+shift+enter
    Shift+enter在当前行之下创建一个空白行,与光标是否在行末无关。Ctrl+shift+enter则在当前行之前插入空白行。
    8. Alt+方向键
    这也是个节省时间的法宝。这个组合将当前行的内容往上或下移动。在try/catch部分,这个快捷方式尤其好使。
    9. ctrl+m
    大显示屏幕能够提高工作效率是大家都知道的。Ctrl+m是编辑器窗口最大化的快捷键。
    10. ctrl+.及ctrl+1:下一个错误及快速修改
    ctrl+.将光标移动至当前文件中的下一个报错处或警告处。这组快捷键我一般与ctrl+1一并使用,即修改建议的快捷键。新版Eclipse的修改建 议做的很不错,可以帮你解决很多问题,如方法中的缺失参数,throw/catch exception,未执行的方法等等。

更多快捷键组合可在Eclipse按下ctrl+shift+L查看。
让我们按照使用频率来看看我最爱用的一些热键组合。(注:以下内容在Eclipse3.02及一上版本通过测试)
1. Control-Shift-T: 打开类型(Open type)。如果你不是有意磨洋工,还是忘记通过源码树(source tree)打开的方式吧。用eclipse很容易打开接口的实现类的,按ctrl+t会列出接口的实现类列表
2. Control-Shift-R: 打开资源(不只是用来寻找Java文件)。小提示:利用Navigator视图的黄色双向箭头按钮让你的编辑窗口和导航器相关联。这会让你打开的文件对应显示在导航器的层级结构中,这样便于组织信息。如果这影响了速度,就关掉它。
3. F3: 打开申明(Open declaration)。或者,利用Declaration Tab(在Java视图模式下,选择Windows --> Show View -- > Declaration)。当你选中代码中的一个方法,然后按这个按键,它会把整个方法在申明方框里显示出来。
4. Alt-left arrow: 在导航历史记录(Navigation History)中后退。就像Web浏览器的后退按钮一样,在利用F3跳转之后,特别有用。(用来返回原先编译的地方)
5. Alt-right arrow: 导航历史记录中向前。
6. Control-Q: 回到最后一次编辑的地方。这个快捷键也是当你在代码中跳转后用的。特别是当你钻的过深,忘记你最初在做什么的时候。
7. Control-Shift-G: 在workspace中搜索引用(reference)。这 是重构的前提。对于方法,这个热键的作用和F3恰好相反。它使你在方法的栈中,向上找出一个方法的所有调用者。一个与此相关的功能是开启“标记”功能 (occurrence marking) 。选择Windows->Preferences->Java-> Editor-> Mark Occurrences,勾选选项。这时,当你单击一个元素的时候,代码中所有该元素存在的地方都会被高亮显示。我个人只使用“标记本地变量”(Mark Local Variables)。注意:太多的高亮显示会拖慢Eclipse。
8. Control-Shift-F: CodeàJavaàPreferencesà根据代码风格设定重新格式化代码。我 们的团队有统一的代码格式,我们把它放在我们的wiki上。要这么做,我们打开Eclipse,选择Window Style,然后设置Code Formatter,Code Style和Organize Imports。利用导出(Export)功能来生成配置文件。我们把这些配置文件放在wiki上,然后团队里的每个人都导入到自己的Eclipse中。
9. Control-O: 快速概要(quick outline)。通过这个快捷键,你可以迅速的跳到一个方法或者属性,只需要输入名字的头几个字母。
10. Control-/: 对一行注释或取消注释。对于多行也同样适用。
11. Control-Alt-down arrow: 复制高亮显示的一行或多行。
12. Alt-down arrow: 将一行或多行向下移动。Alt-up arrow会向上移动。
其他的热键在菜单里有。你可以通过按下Control-Shift-L(从3.1版本开始), 看到所有快捷键的列表。按下Control-Shift-L两次,会显示热键对话框(Keys Preferences dialog),你可以在这里自己设置热键。我欢迎你在Talkback部分发表你的Eclipse提示。
其他的Eclipse窍门
我总结了几个相关的小窍门:
锁定命令行窗口:在命令行视图中(Window ->Show View ->Other ->Basic ->Console),试试看用滚动锁定按钮来锁定控制台输出不要滚屏。
使用Ant视图: 在我的Java或Debug模式下,我喜欢显示出Ant视图,这样我就可以迅速的运行Ant任务。通过Window Ant可以找到该视图。把Ant视图放在屏幕的一角, 通过“添加编译文件(Addà Other à Show View à Buildfiles)”按钮来添加build.xml文件。在3.1版本中,甚至支持Ant调试脚本语言。
自动遍历一个集合:for + Control-Space: 如果你还不知道,那么你应该记住Control-Space是自动完成功能。在Eclipse中,你还可以自动完成结构。在一个数组或集合范围内,试试看 输入“for”然后按下Control-Space键。Eclipse会问你你想要遍历哪一个集合然后自动完成循环代码。
使用分级布局: 在包浏览视图(Package Explorer view)中默认的布局(扁平式)方式让我困惑,它把包的全名显示在导航树(navigation tree)中。我更喜欢我源码的包和文件系统视图,在Eclipse中叫做分级布局(Hierarchical Layout)。要切换到这种模式,点击包浏览视图中向下的按钮,选择布局(Layout),然后选择分级(Hierarchial)。
一次显示多个文件:你可以一次浏览多个文件。把不在激活状态的编辑窗口拖到激活窗口的底部或侧边的滚动条上,就可以打开该编辑窗口。这是我能描述该窍门的最好方式了。
同时打开两个Eclipse: 要将改动从一个CVS分支上合并到另外一个上,我喜欢通过同时打开两个工作目录(Workspace)不同Eclipse来实现。这样我可以通过比较 CVS上的最新版本看到所有的变化(右键单击工程,然后选择Compare Lastest from HEAD)然后把每一个变化都合并到另外一个CVS分支上。启动多个Eclipse的最简单的方法是利用Eclipseàwith Launcher。
Implementors插件:安装一个能够跳到一个接口的实现的插件。如果你是个dependency injection 粉丝,或者正在基于编写优良的接口工作,那么你需要一个这样的插件来加速代码导航。 你可以在SourceForge找到这个插件。
Ctrl+Alt+H
如果你想知道一个类的方法到底被那些其他的类调用,那么请选中这个方法名,然后按“Ctrl+Alt+H”,
Eclipse就会显示出这个方法被哪些方法调用,最终产生一个调用关系树。 
1. Ctrl+左键

这个是大多数人经常用到的,用来查看变量、方法、类的定义
2. Ctrl+O
查看一个类的纲要,列出其方法和成员变量。提示:再多按一次Ctrl+O,可以列出该类继承的方法和变量。
助记:"O"--->"Outline"--->"纲要"
3. Ctrl+T
查看一个类的继承关系树,是自顶向下的,再多按一次Ctrl+T, 会换成自底向上的显示结构。
提示:选中一个方法名,按Ctrl+T,可以查看到有这个同名方法的父类、子类、接口。
助记:"T"------->"Tree"----->"层次树"
4.Alt+左右方向键
我们经常会遇到看代码时Ctrl+左键,层层跟踪,然后迷失在代码中的情况,这时只需要按“Alt+左方向键
”就可以退回到上次阅读的位置,同理,按“Alt+右方向键”会前进到刚才退回的阅读位置,就像浏览器的
前进和后退按钮一样。
导入包:Ctrl+Shift+O 
编辑 
作用域 功能 快捷键 
全局 查找并替换 Ctrl+F 
文本编辑器 查找上一个 Ctrl+Shift+K 
文本编辑器 查找下一个 Ctrl+K 
全局 撤销 Ctrl+Z 
全局 复制 Ctrl+C 
全局 恢复上一个选择 Alt+Shift+↓ 
全局 剪切 Ctrl+X 
全局 快速修正 Ctrl1+1 
全局 内容辅助 Alt+/ 
全局 全部选中 Ctrl+A 
全局 删除 Delete 
全局 上下文信息 Alt+? 
Alt+Shift+? 
Ctrl+Shift+Space 
Java编辑器 显示工具提示描述 F2 
Java编辑器 选择封装元素 Alt+Shift+↑ 
Java编辑器 选择上一个元素 Alt+Shift+← 
Java编辑器 选择下一个元素 Alt+Shift+→ 
文本编辑器 增量查找 Ctrl+J 
文本编辑器 增量逆向查找 Ctrl+Shift+J 
全局 粘贴 Ctrl+V 
全局 重做 Ctrl+Y 
查看 
作用域 功能 快捷键 
全局 放大 Ctrl+= 
全局 缩小 Ctrl+- 
窗口 
作用域 功能 快捷键 
全局 激活编辑器 F12 
全局 切换编辑器 Ctrl+Shift+W 
全局 上一个编辑器 Ctrl+Shift+F6 
全局 上一个视图 Ctrl+Shift+F7 
全局 上一个透视图 Ctrl+Shift+F8 
全局 下一个编辑器 Ctrl+F6 
全局 下一个视图 Ctrl+F7 
全局 下一个透视图 Ctrl+F8 
文本编辑器 显示标尺上下文菜单 Ctrl+W 
全局 显示视图菜单 Ctrl+F10 
全局 显示系统菜单 Alt+- 
导航 
作用域 功能 快捷键 
Java编辑器 打开结构 Ctrl+F3 
全局 打开类型 Ctrl+Shift+T 
全局 打开类型层次结构 F4 
全局 打开声明 F3 
全局 打开外部javadoc Shift+F2 
全局 打开资源 Ctrl+Shift+R 
全局 后退历史记录 Alt+← 
全局 前进历史记录 Alt+→ 
全局 上一个 Ctrl+, 
全局 下一个 Ctrl+. 
Java编辑器 显示大纲 Ctrl+O 
全局 在层次结构中打开类型 Ctrl+Shift+H 
全局 转至匹配的括号 Ctrl+Shift+P 
全局 转至上一个编辑位置 Ctrl+Q 
Java编辑器 转至上一个成员 Ctrl+Shift+↑ 
Java编辑器 转至下一个成员 Ctrl+Shift+↓ 
文本编辑器 转至行 Ctrl+L 
搜索 
作用域 功能 快捷键 
全局 出现在文件中 Ctrl+Shift+U 
全局 打开搜索对话框 Ctrl+H 
全局 工作区中的声明 Ctrl+G 
全局 工作区中的引用 Ctrl+Shift+G 
文本编辑 
作用域 功能 快捷键 
文本编辑器 改写切换 Insert 
文本编辑器 上滚行 Ctrl+↑ 
文本编辑器 下滚行 Ctrl+↓ 
文件 
作用域 功能 快捷键 
全局 保存 Ctrl+X 
Ctrl+S 
全局 打印 Ctrl+P 
全局 关闭 Ctrl+F4 
全局 全部保存 Ctrl+Shift+S 
全局 全部关闭 Ctrl+Shift+F4 
全局 属性 Alt+Enter 
全局 新建 Ctrl+N 
项目 
作用域 功能 快捷键 
全局 全部构建 Ctrl+B 
源代码 
作用域 功能 快捷键 
Java编辑器 格式化 Ctrl+Shift+F 
Java编辑器 取消注释 Ctrl+/ 
Java编辑器 注释 Ctrl+/ 
Java编辑器 添加单个import Ctrl+Shift+M 
Java编辑器 组织多个import Ctrl+Shift+O 
Java编辑器 使用try/catch块来包围 未设置,太常用了,所以在这里列出,建议自己设置。 
也可以使用Ctrl+1自动修正。 
调试/运行 
作用域 功能 快捷键 
全局 单步返回 F7 
全局 单步跳过 F6 
全局 单步跳入 F5 
全局 单步跳入选择 Ctrl+F5 
全局 调试上次启动 F11 
全局 继续 F8 
全局 使用过滤器单步执行 Shift+F5 
全局 添加/去除断点 Ctrl+Shift+B 
全局 显示 Ctrl+D 
全局 运行上次启动 Ctrl+F11 
全局 运行至行 Ctrl+R 
全局 执行 Ctrl+U 
重构 
作用域 功能 快捷键 
全局 撤销重构 Alt+Shift+Z 
全局 抽取方法 Alt+Shift+M 
全局 抽取局部变量 Alt+Shift+L 
全局 内联 Alt+Shift+I 
全局 移动 Alt+Shift+V 
全局 重命名 Alt+Shift+R 
全局 重做 Alt+Shift+Y
(1)Ctrl+M切换窗口的大小 
(2)Ctrl+Q跳到最后一次的编辑处 
(3)F2当鼠标放在一个标记处出现Tooltip时候按F2则把鼠标移开时Tooltip还会显示即Show Tooltip
Description。 
F3跳到声明或定义的地方。 
F5单步调试进入函数内部。 
F6单步调试不进入函数内部,如果装了金山词霸2006则要把“取词开关”的快捷键改成其他的。 
F7由函数内部返回到调用处。 
F8一直执行到下一个断点。 
(4)Ctrl+Pg~对于XML文件是切换代码和图示窗口 
(5)Ctrl+Alt+I看Java文件中变量的相关信息 
(6)Ctrl+PgUp对于代码窗口是打开“Show List”下拉框,在此下拉框里显示有最近曾打开的文件 
(7)Ctrl+/ 在代码窗口中是这种//~注释。 
Ctrl+Shift+/ 在代码窗口中是这种/*~*/注释,在JSP文件窗口中是〈!--~--〉。 
(8)Alt+Shift+O(或点击工具栏中的Toggle Mark Occurrences按钮) 当点击某个标记时可使本页面中其他
地方的此标记黄色凸显,并且窗口的右边框会出现白色的方块,点击此方块会跳到此标记处。 
(9)右击窗口的左边框即加断点的地方选Show Line Numbers可以加行号。 
(10)Ctrl+I格式化激活的元素Format Active Elements。 
Ctrl+Shift+F格式化文件Format Document。 
(11)Ctrl+S保存当前文件。 
Ctrl+Shift+S保存所有未保存的文件。 
(12)Ctrl+Shift+M(先把光标放在需导入包的类名上) 作用是加Import语句。 
Ctrl+Shift+O作用是缺少的Import语句被加入,多余的Import语句被删除。 
(13)Ctrl+Space提示键入内容即Content Assist,此时要将输入法中Chinese(Simplified)IME-
Ime/Nonlme Toggle的快捷键(用于切换英文和其他文字)改成其他的。 
Ctrl+Shift+Space提示信息即Context Information。 
(14)双击窗口的左边框可以加断点。 
(15)Ctrl+D删除当前行。
Eclipse快捷键大全 
Ctrl+1 快速修复(最经典的快捷键,就不用多说了) 
Ctrl+D: 删除当前行 
Ctrl+Alt+↓ 复制当前行到下一行(复制增加) 
Ctrl+Alt+↑ 复制当前行到上一行(复制增加)
Alt+↓ 当前行和下面一行交互位置(特别实用,可以省去先剪切,再粘贴了) 
Alt+↑ 当前行和上面一行交互位置(同上) 
Alt+← 前一个编辑的页面 
Alt+→ 下一个编辑的页面(当然是针对上面那条来说了)
Alt+Enter 显示当前选择资源(工程,or 文件 or文件)的属性
Shift+Enter 在当前行的下一行插入空行(这时鼠标可以在当前行的任一位置,不一定是最后) 
Shift+Ctrl+Enter 在当前行插入空行(原理同上条)
Ctrl+Q 定位到最后编辑的地方 
Ctrl+L 定位在某行 (对于程序超过100的人就有福音了) 
Ctrl+M 最大化当前的Edit或View (再按则反之) 
Ctrl+/ 注释当前行,再按则取消注释 
Ctrl+O 快速显示 OutLine 
Ctrl+T 快速显示当前类的继承结构 
Ctrl+W 关闭当前Editer 
Ctrl+K 参照选中的Word快速定位到下一个 
Ctrl+E 快速显示当前Editer的下拉列表(如果当前页面没有显示的用黑体表示)
Ctrl+/(小键盘) 折叠当前类中的所有代码
Ctrl+×(小键盘) 展开当前类中的所有代码
Ctrl+Space 代码助手完成一些代码的插入(但一般和输入法有冲突,可以修改输入法的热键,也可以暂用
Alt+/来代替)
Ctrl+Shift+E 显示管理当前打开的所有的View的管理器(可以选择关闭,激活等操作)
Ctrl+J 正向增量查找(按下Ctrl+J后,你所输入的每个字母编辑器都提供快速匹配定位到某个单词,如果没有
,则在stutes line中显示没有找到了,查一个单词时,特别实用,这个功能Idea两年前就有了)
Ctrl+Shift+J 反向增量查找(和上条相同,只不过是从后往前查)
Ctrl+Shift+F4 关闭所有打开的Editer
Ctrl+Shift+X 把当前选中的文本全部变味小写
Ctrl+Shift+Y 把当前选中的文本全部变为小写
Ctrl+Shift+F 格式化当前代码
Ctrl+Shift+P 定位到对于的匹配符(譬如{}) (从前面定位后面时,光标要在匹配符里面,后面到前面,则反之
)
下面的快捷键是重构里面常用的,本人就自己喜欢且常用的整理一下(注:一般重构的快捷键都是Alt+Shift开
头的了)
Alt+Shift+R 重命名 (是我自己最爱用的一个了,尤其是变量和类的Rename,比手工方法能节省很多劳动力)
Alt+Shift+M 抽取方法 (这是重构里面最常用的方法之一了,尤其是对一大堆泥团代码有用)
Alt+Shift+C 修改函数结构(比较实用,有N个函数调用了这个方法,修改一次搞定)
Alt+Shift+L 抽取本地变量( 可以直接把一些魔法数字和字符串抽取成一个变量,尤其是多处调用的时候)
Alt+Shift+F 把Class中的local变量变为field变量 (比较实用的功能)
Alt+Shift+I 合并变量(可能这样说有点不妥Inline) 
Alt+Shift+V 移动函数和变量(不怎么常用) 
Alt+Shift+Z 重构的后悔药(Undo)
链接http://hi.baidu.com/lzycsd/blog/item/dcce5989a3f559bb0f2444cb.html

相比之前的Win7和Win8/8.1,Win10在传统桌面上做出了很多改动,相应的快捷键也发生了一些变化。下面我们就将它们集中汇总一下,看一看你都知道么!
软件名称:
Windows10技术预览版
软件版本:
官方简体中文版
软件大小:
3769.53MB
软件授权:
免费
适用平台:
WinXP Win2003 Vista Win8 Win7
下载地址:
图01 Win10快捷键!你都知道么
  Win+C:强制调出Charm栏。
  Win+左/右/上/左上/左下/右上/右下:将窗口快速缩放至1/2分屏、1/4分屏,其中1/4分屏为Win10新增功能,对应快捷键为Win+左上/左下/右上/右下。
图02 左侧为1/2分屏,右上方为1/4分屏
  Win+E:调出“主页”窗口。“主页”是Win10新增功能,包含最近访问文件夹、最近访问文件、收藏夹等,以资源管理器作为载体。
  Win+Home:仅保留当前窗口,其余窗口最小化。
  Win+Enter:直接启动“讲述人”。
  Win+数字键:快速打开任务栏上已固定应用,比如Win+1打开第一个应用、Win+2打开第二个应用……,以此类推,同时支持Metro应用。
  Win+T:在任务栏图标间切换,但不打开,相当于鼠标悬停。
  Alt+Tab:窗口切换器,当然Win10已经将它改进了,新版缩略图更大,并且是全屏显示。
图03 Alt+Tab没变,但效果却大不一样了!
  Win+Tab:Win7时代的Flip3D(3D窗口切换器),到了Win10中变成了虚拟桌面切换器。嗯!和点击任务栏上那个按钮一个效果!
图04 “虚拟桌面”取代之前的Flip3D
  Ctrl+Shift+ESC:直接打开任务管理器。
  Alt+左/右/上:快速跳转至上一个文件夹/下一个文件夹/父文件夹(仅限资源管理器)。
  F11:沉浸式传统窗口布局(仅限传统程序)。
图05 沉浸式桌面窗口
  Win+Ctrl+左/右:切换当前虚拟桌面。
  Win+Ctrl+D:建立新虚拟桌面。
  Win+Ctrl+F4:关闭当前虚拟桌面(已打开窗口会自动移动到下一个桌面,不会丢失!)。
  Win+D:显示传统桌面(即窗口最小化,沉浸式Metro需按动两下)。
  Win+I:打开Charm栏→设置菜单(非新版Charm菜单)。
  Win+X:打开简易版开始菜单,这个用来启动一些系统级程序还是很方便的。
图06 简易版开始菜单!大家还记得么
  Win+Prt Sc:屏幕截图。按下该快捷键后,屏幕会瞬间暗一下,并伴有“咔嚓”一声,视觉感和手机上的截屏软件很像,截图自动存放至“图片”文件夹。
图07 Win+Prt Sc截下的图
  Ctrl+V:这是Win10命令提示符里新增的,功能就是将剪切板内容直接粘贴到提示符内。而在此之前,这组快捷键所能得到的结果,仅仅是一个^v。
  以上是笔者结合自己经验,挑选的一些日常使用率较高的快捷键,诸如大家熟悉的Win+L、Win+R之类并未涉及。当然Win10是一款全新系统,短短的一两天试用是不可能将其探寻完整的。如果你在试用过程中也发现了一些不一样的按钮,也欢迎和我们一同分享,祝大家Win10玩得愉快!

深入解析 DB2 —— 高级管理、内部体系结构与诊断案例,第 6 章

高级锁
牛新庄
2009 年 7 月 08 日发布
Weibo
Google+
用电子邮件发送本页面
 1

系列内容:

此内容是该系列的一部分:developerWorks 图书频道

我们在进行客户支持时遇到最多的话题之一就是锁。“为什么 DB2 锁住了这个表、行或者对象?”,“这个锁会阻塞多长时间及为什么?”;“为什么出现了死锁?”,“我的锁请求在等待什么?”,诸如此类问题等等。更仔细地分析一些常见的锁示例可以说明 DB2 锁定策略背后的原则。在国内很多 DB2 用户都会碰到有关锁等待、死锁和锁升级等锁相关的问题,本章将会对这些问题以及解决方法做详细的讲解。
本章主要讲解如下内容:
隔离级别和锁
加锁总结
乐观锁
内部锁
设置锁相关的注册变量

6.1  隔离级别和锁

要维护数据库的一致性和数据完整性,同时又允许多个应用程序同时访问一个数据库,将这样的特性称为并发性。 DB2 数据库尝试强制实施并发性的方法之一是使用隔离级别,它决定在第一个事务访问数据时,如何对其他事务锁定或隔离该事务所使用的数据。 DB2 使用下列隔离级别来强制实施并发性:
可重复读 (Reapeatable Read,RR)
读稳定性 (Read Stability,RS)
游标稳定性 (Cursor Stability,CS)
未提交的读 (Uncommitted Read,UR)
隔离级别是根据称为现象 (Phenomena) 的三个禁止操作序列来声明的:
脏读 (Dirty Read):在事务 A 提交修改结果之前,其他事务即可看到事务A的修改结果。
不可重复读 (Non-Repeatable Read):在事务A提交之前,允许其他事务修改和删除事务A涉及的数据,导致事务A中执行同样操作的结果集变小。
幻像读 (Phantom Read):事务A在提交查询结果之前,其他事务可以插入或者更改事务 A 涉及的数据,导致事务 A 中执行同样操作的结果集增大。
数据库并发性 ( 可以同时访问同一资源的事务数量 ) 因隔离级别不同而有所差异,可重复读隔离级别可以防止所有现象,但是会大大降低并发性。未提交读隔离级别提供了最大的并发性,但可能会造成“脏读”、“幻像读”或“不可重复读”现象。 DB2 默认的隔离级别是 CS 。

6.1.1  可重复读

可重复读隔离级别是最严格的隔离级别。在使用它时,一个事务的操作结果完全与其他并发事务隔离,脏读、不可重复读、幻像读都不会发生。当使用可重复读隔离级别时,在事务执行期间会共享 (S) 锁定该事务以任何方式引用的所有行,在该事务中多次执行同一条 SELECT 语句,得到的结果数据集总是相同的。因此,使用可重复读隔离级别的事务可以多次检索同一行集,并可以对它们执行任意操作,直到提交或回滚操作终止事务。但是,在事务提交前,不允许其他事务执行会影响该事务正在访问的任何行的插入、更新或删除操作。为了确保这种行为,需要锁定该事务所引用的每一行—— 而不是仅锁定被实际检索或修改的那些行。因此,如果一个表中有 1000 行,但只检索两行,则整个表 (1000 行,而不仅是被检索的两行 ) 都会被锁定。输出结果如下:

1
2
3
4
5
6
C:\>db2 +c select empno,firstnme,salary from employee where empno
between '000010' and '000020' withrrEMPNO FIRSTNME  SALARY
 ------ ------------ -----------
 000010 CHRISTINE  152750.00
 000020 MICHAEL  94250.00
  2 条记录已选择。

我们通过“ get snapshot for locks on sample ”命令来监控表加锁情况,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
C:\>db2 update monitor switches using lock on
 DB20000I  UPDATE MONITOR SWITCHES 命令成功完成。
 C:\>db2 get snapshot for locks on sample | more
 -------------- 略 ------------------
锁定列表
锁定名称  = 0x020006000E0040010000000052
锁定属性  = 0x00000010
发行版标志 = 0x00000004
锁定计数  = 1
挂起计数  = 0
锁定对象名  = 20971534
对象类型=表
表空间名= USERSPACE1表模式= DB2ADMIN表名= EMPLOYEE方式= S  --注:虽然读取了两行,但是整个表加S锁

如果使用这种隔离级别,不管你从表中读多少数据,整个表上都加 S 锁,直到该事务被提交或回滚,表上的锁才会被释放。这样可以保证在一个事务中即使多次读取同一行,都会得到相同结果集。另外,在同一事务中如果以同样的搜索标准重新打开已被处理过的游标,那么得到的结果集不会改变。可重复读相对于读稳定性而言,加锁的范围更大:对于读稳定性,应用程序只对符合要求的所有行加锁;而对于重复读,应用程序将对整个表都加 S 锁。
可重复读会锁定应用程序在工作单元中引用的整个表。利用可重复读,一个应用程序在打开游标的相同工作单元内发出一个 SELECT 语句两次,每次都返回相同的结果。利用可重复读隔离级别,不可能出现丢失更新、脏读和幻像读的情况。
在该工作单元完成之前,“可重复读”应用程序可以多次检索和操作这些行。但是,在该工作单元完成之前其他应用程序均不能更新、删除或插入可能会影响结果表的行。“可重复读”应用程序不能查看其他应用程序未提交的更改。

6.1.2  读稳定性

读稳定性隔离级别没有可重复读隔离级别那么严格;因此,它没有将事务与其他并发事务的效果完全隔离。读稳定性隔离级别可以防止脏读和不可重复读,但是可能出现幻像读。在使用这个隔离级别时,只锁定事务实际检索和修改的行。因此,如果一个表中有 1000 行,但只检索两行 ( 通过索引扫描 ),则只锁定被检索的两行 ( 而不是所扫描的 1000 行 ) 。因此,如果在同一个事务中发出同一个 SELECT 语句两次或更多次,那么每次产生的结果数据集可能不同。
与可重复读隔离级别一样,在读稳定性隔离级别下运行的事务可以检索一个行集 (ROWS SET),并可以对它们执行任意操作,直到事务终止。在这个事务存在期间,其他事务不能执行那些会影响这个事务检索到的行集的更新或删除操作,但是可以执行插入操作。如果插入的行与第一个事务的查询的选择条件匹配,那么这些行可能作为幻像出现在后续产生的结果数据集中。其他事务对其他行所作的更改,在提交之前是不可见的。下面我们还用上面的那个例子锁定读稳定性,输出结果如下:

1
2
3
4
5
6
C:\>db2 +c select empno,firstnme,salary from employee where empno
between '000010' and '000020' withrsEMPNO FIRSTNME  SALARY
 ------ ------------ -----------
 000010 CHRISTINE  152750.00
 000020 MICHAEL  94250.00
  2 条记录已选择。

我们通过“ get snapshot for locks on sample ”命令来监控表加锁情况,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
C:\>db2 update monitor switches using lock on
 DB20000I  UPDATE MONITOR SWITCHES 命令成功完成。
 C:\>db2 get snapshot for locks on sample | more
 -------------- 略 ------------------
锁定列表
锁定名称    = 0x02000600050040010000000052
锁定属性    = 0x00000010
发行版标志    = 0x00000001
锁定计数    = 1
挂起计数    = 0
锁定对象名    = 20971525
对象类型  = 行
表名        =EMPLOYEE方式    =S --注:只在读取的行上加S锁
锁定名称    = 0x02000600040040010000000052
锁定属性    = 0x00000010
发行版标志    = 0x00000001
锁定计数    = 1
挂起计数    = 0
锁定对象名    = 20971524
对象类型  = 行
表名        =EMPLOYEE方式= S --注:只在读取的行上加S锁
锁定名称  = 0x02000600000000000000000053
锁定属性  = 0x00000010
发行版标志  = 0x00000001
锁定计数  = 1
挂起计数             = 0
锁定对象名  = 6
对象类型=表
表名       = EMPLOYEE方式= IS --注:表上加IS锁

如果使用这种隔离级,那么在一个事务中将有 N+1 个锁,其中 N 是所有被读取 ( 通过索引扫描 ) 过的行的数目,这些行上都会被加上 NS 锁,在表上加上 1 个 IS 锁。这些锁直到该事务被提交或回滚才会被释放。这样可以保证在一个事务中即使多次读取同一行,得到的值也不会改变。但是使用这种隔离级别,在一个事务中,如果使用同样的搜索标准重新打开已被处理过的游标,则结果集可能改变 ( 可能会增加某些行,这些行被称为幻影行 (Phantom)) 。这是因为 RS 隔离级别不能阻止通过插入或更新操作在结果集中加入新行。
注意:
NS 是下一键共享锁,此时锁拥有者和所有并发的事务都可以读 ( 但不能更改 ) 被锁定行中的数据。这种锁用来在使用读稳定性或游标稳定性事务隔离级别读取的数据上代替共享锁。
读稳定性 (RS) 只锁定应用程序在工作单元中检索的那些行。它确保在某个工作单元完成之前,在该工作单元运行期间的任何限定行读取不被其他应用程序进程更改,且确保不会读取由另一个应用程序进程所更改的任何行,直至该进程提交了这些更改。也就是说,不可能出现“不可重复读”情形。
“读稳定性”隔离级别的其中一个目标是提供较高并行性以及数据的稳定视图,为了有助于达到此目标,优化器确保在发生锁定升级前不获取表级锁定。
“读稳定性”隔离级别最适用于包括下列所有特征的应用程序:
在并发环境下运行。
需要限定某些行在工作单元运行期间保持稳定。
在工作单元中不会多次发出相同的查询,或者在同一工作单元中发出多次查询时并不要求该查询获得相同的回答。

6.1.3  游标稳定性

游标稳定性隔离级别在隔离事务效果方面非常宽松。它可以防止脏读;但有可能出现不可重复读和幻像读。这是因为在大多数情况下,游标稳定性隔离级别只锁定事务声明并打开的游标当前引用的行。
当使用游标稳定性隔离级别的事务通过游标从表中检索行时,其他事务不能更新或删除游标所引用的行。但是,如果被锁定的行本身不是用索引访问的,那么其他事务可以将新的行添加到表中,以及对被游标锁定行前后的行进行更新或删除操作。所获取的锁一直有效,直到游标重定位或事务终止为止 ( 如果游标重定位,原来行上的锁就被释放,并获得游标现在引用的行上的锁 ) 。此外,如果事务修改了它检索到的任何行,那么在事务终止之前,其他事务不能更新或删除该行,即使在游标不再位于被修改的行时。与可重复读和读稳定性隔离级别一样,其他事务在其他行上进行的更改,在这些更改提交之前对于使用游标稳定性隔离级别的事务 ( 这是默认的隔离级别 ) 是不可见的。我们还用上面那个例子,一个表中有 1000 行数据,我们只检索其中两行数据。那么对于可重复读隔离级别会锁住整个表,对于读稳定性隔离级别会对读到的数据 ( 两行 ) 加锁,而对于游标稳定性隔离级别只对游标当前所在那一行加锁,游标所在行的前一行和下一行都不加锁。下面我们举一个游标稳定性的例子,输出结果如下:

1
2
3
4
5
6
7
8
9
C:\>db2 +c declare c1 cursor for  select empno,firstnme,salary from employee
where empno between '000010' and '000020' with cs
 C:\>db2 +c open c1
 C:\>db2 +c fetch c1
 EMPNO FIRSTNME   SALARY
 ------ ------------ -----------
 000010 CHRISTINE 152750.00--注:游标当前所在行,DB2只对这一行加锁。游标的
                上一行和下一行都不加锁。当游标移动到下一行时,锁自动释放。
  1 条记录已选择。

我们通过“ get snapshot for locks on sample ”命令来监控表加锁情况,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
C:\>db2 update monitor switches using lock on
 DB20000I  UPDATE MONITOR SWITCHES 命令成功完成。
 C:\>db2 get snapshot for locks on sample | more
 -------------- 略 ------------------
锁定名称   = 0x02000600040040010000000052
锁定属性   = 0x00000010
发行版标志   = 0x00000001
锁定计数   = 1
挂起计数   = 0
锁定对象名   = 20971524
对象类型=行
表名= EMPLOYEE方式= S --注:只在游标所在行上加S锁
锁定名称   = 0x02000600000000000000000053
锁定属性   = 0x00000010
发行版标志   = 0x00000001
锁定计数   = 1
挂起计数   = 0
锁定对象名   = 6
对象类型=表
表名= EMPLOYEE方式= IS --注:表上加IS锁

如果使用这种隔离级,那么在一个事务中只有两个锁:结果集中只有正在被读取的那一行 ( 游标指向的行 ) 被加上 NS 锁,在表上加 IS 锁。其他未被处理的行上不加锁。这种隔离级别只能保证正在处理的行的值不会被其他并发的程序所改变。该隔离级别是 DB2 默认的隔离级别。
游标稳定性 (CS) 当在行上定位游标时会锁定任何由应用程序的事务所访问的行。此锁定在读取下一行或终止事务之前有效。但是,如果更改了某一行上的任何数据,那么在对数据库提交更改之前必须挂起该锁定。
对于具有“游标稳定性”的应用程序已检索的行,当该行上有任何可更新的游标时,任何其他应用程序都不能更新或删除该行。“游标稳定性”应用程序不能查看其他应用程序的未提交操作。
使用“游标稳定性”,可能会出现不可重复读和幻像读现象。“游标稳定性”是默认隔离级别,应在需要最大并行性,但只看到其他应用程序中的已提交行的情况下才使用。

6.1.4  未提交读

未提交读隔离级别是最不严格的隔离级别。实际上,在使用这个隔离级别时,仅当另一个事务试图删除或更改被检索的行所在的表时,才会锁定一个事务检索的行。因为在使用这种隔离级别时,行通常保持未锁定状态,所以脏读、不可重复读和幻像读都可能会发生。因此,未提交读隔离级别通常用于那些访问只读表和视图的事务,以及某些执行 SELECT 语句的事务 ( 只要其他事务的未提交数据对这些语句没有负面效果 ) 。
顾名思义,其他事务对行所做的更改在提交之前对于使用未提交读隔离级别的事务是可见的。但是,此类事务不能看见或访问其他事务 DDL(CREATE、ALTER 和 DROP) 语句所创建的表、视图或索引,直到那些事务被提交为止。类似地,如果其他事务删除了现有的表、视图或索引,那么仅当进行删除操作的事务终止时,使用未提交读隔离级别的事务才能知道这些对象不再存在了。
一定要注意一点:当运行在未提交读隔离级别下的事务使用可更新游标时,该事务的行为和在游标稳定性隔离级别下运行一样,并应用游标稳定性隔离级别的约束。下面我们举一个例子。
我们编写一个 SQL 存储过程,在存储过程中我们显式地在 SELECT 语句中使用 UR 隔离级别。
创建一个存储过程,保存为 LOCKS.SQL,输出结果如下:

1
2
3
4
5
6
7
8
CREATE PROCEDURE locks()
 LANGUAGE SQL
 BEGIN
 declare c1 cursor for select * from staff with UR;
 open c1;
 while 1=1 do  ——注:死循环
 end while;
 END @

为了方便抓住锁信息,我们在这个存储过程的结尾处使用了一个死循环。利用一个命令窗口运行存储过程,输出结果如下:

1
2
C:\ >db2 – td@ -vf locks.sql
 C:\ >db2 "call locks()"

再打开一个新的窗口,得到在 STAFF 表上的当前锁信息,输出结果如下:

1
2
3
4
5
C:\>db2pd -db sample -locks show detail
 Locks:
 Address TranHdl Lockname Type Mode Sts Owner Dur HldCnt Att ReleaseFlg
 0x408E0290 2 00020003000000000000000054 Table .ISG 2 1 0 0x0000 0x00000001
 TbspaceID 2 TableID 3

但是会发现此时在 STAFF 表上出现的是 IS 锁,而不是 IN 锁。是什么原因呢?这是因为 UR 隔离级别允许应用程序存取其他事务的未落实的更改,但是对于只读和可更新这两种不同的游标类型,UR 的工作方式有所不同。对于可更新的游标,当它使用隔离级别 UR 运行程序时,应用程序会自动使用隔离级别 CS 。
在上面的例子当中,虽然显式地指定了 SQL 语句的隔离级别是 UR,但是,由于在存储过程中使用的游标是模糊游标 ( 也就是没有显式地声明游标是只读的还是可更新的 ),因而系统会默认地将这个模糊游标当成可更新游标处理,存储过程的隔离级别自动从 UR 升级为 CS 。要防止此升级,可以采用以下办法:
修改应用程序中的游标,以使这些游标是非模糊游标。将 SELECT 语句更改为包括 FOR READ ONLY 子句。
将模糊游标保留在应用程序中,但是预编译程序或使用 BLOCKING ALL 和 STATIC READONLY YES 选项绑定它以允许在运行该程序时将任何模糊游标视为只读游标。
我们还是使用上面的例子,显式地将该游标声明成只读游标,输出结果如下:

1
declare c1 cursor for select * from stafffor read onlywith UR;

此时我们再运行这个存储过程,并利用 DB2PD 获取锁的情况,输出结果如下:

1
2
3
4
5
c:\> db2pd -db sample -locks show locks
 Locks:
 Address TranHdl Lockname Type Mode Sts Owner Dur HldCnt Att ReleaseFlg
 0x408E07E0 2 00020003000000000000000054 Table.ING 2 1 0 0x0000 0x00000001
 TbspaceID 2 TableID 3

-注:可以看到STAFF表上出现的锁是IN锁。
从上面的例子中我们可以看到:“未提交读 (UR) ”隔离级别允许应用程序访问其他事务的未提交的更改。除非其他应用程序尝试删除或改变该表,否则该应用程序也不会锁定正读取的行而使其他应用程序不能访问该行。对于只读和可更新的游标,“未提交的读”的工作方式有所不同。
如果使用这种隔离级别,那么对于只读操作不加行锁。典型的只读操作包括: SELECT 语句的结果集只读 ( 比如语句中包括 ORDER BY 子句 ) ;定义游标时指明起为 FOR FETCH ONLY 或 FOR READ ONLY 。
该隔离级别可以改善应用程序的性能,同时可以达到最大程度的并发性。但是,应用程序的数据完整性将受到威胁。如果需要读取未提交的数据,该隔离级是唯一选择。
使用“未提交的读”,可能出现不可重复读行为和幻像读现象。“未提交读”隔离级别最常用于只读表上的查询,或者在仅执行选择语句且不关心是否可从其他应用程序中看到未提交的数据时也最常用。
以上我们所讲的隔离级别的加锁范围和持续时间都是针对读操作而言的。对于更改操作,被修改的行上会被加上 X 锁,无论使用何种隔离级别,X 锁都直到提交或回滚之后才会被释放。

6.1.5  隔离级别加锁示例讲解

假设有一张表 EMP1,表中有 42 条记录,我们使用 FOR READ ONLY 分别在 UR、CS、RS 和 RR 隔离级别下加锁。
EMP1 表在本章后续的内容中也会使用到,其创建过程如下:

1
2
C:\> db2 "create table emp1 like employee"
 C:\> db2 "insert into emp1 select * from employee"

我们使用 EMP1 表中 JOB 字段内容为 'CLERK' 的数据,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
C:\>db2 +c select empno,job,salary from emp1 where job='CLERK' 
for read only  EMPNO JOB  SALARY
 ------ -------- -----------
 000120 CLERK  49250.00
 000230 CLERK  42180.00
 000240 CLERK  48760.00
 000250 CLERK  49180.00
 000260 CLERK  47250.00
 000270 CLERK    37380.00
 200120 CLERK  39250.00
 200240 CLERK  37760.00
  8 条记录已选择。

在上面的 SQL 语句中,我们从表的 42 条记录中返回 8 条记录。下面我们分别看看这条语句在不同的隔离级别下加锁的情况:
UR 隔离级别,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
C:\>db2 +c select empno,job,salary from emp1 where job='CLERK' 
for read onlywith urEMPNO JOB    SALARY
 ------ -------- -----------
 000120 CLERK  49250.00
 000230 CLERK  42180.00
 000240 CLERK  48760.00
 000250 CLERK  49180.00
 000260 CLERK  47250.00
 000270 CLERK  37380.00
 200120 CLERK  39250.00
 200240 CLERK  37760.00
  8 条记录已选择。

在另外一个窗口中使用“ db2 get snapshot for locks on sample ”命令监控,发现在 UR 隔离级别下,在表上有一个 IN 锁,没有加任何行锁。
CS 隔离级别,输出结果如下:

1
2
3
4
5
6
C:\>db2 +c declare c1 cursor for select empno,job,salary from emp1 
where job='CLERK'  for read onlywith CSC:\>db2 +c open c1C:\>db2 +c fetch c1
 EMPNO JOB     SALARY
 ------ -------- -----------
 000120 CLERK  49250.00
  1 条记录已选择。

在另外一个窗口中使用“ db2 get snapshot for locks on sample ”命令监控,发现在 CS 隔离级别下,共有两个锁:在表上有一个 IS 锁,在行上有一个 NS 锁。
RS 隔离级别,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
C:\>db2 +c select empno,job,salary from emp1 where job='CLERK' 
for read onlywith RSEMPNO JOB  SALARY
 ------ -------- -----------
 000120 CLERK  49250.00
 000230 CLERK  42180.00
 000240 CLERK  48760.00
 000250 CLERK  49180.00
 000260 CLERK  47250.00
 000270 CLERK  37380.00
 200120 CLERK  39250.00
 200240 CLERK  37760.00
  8 条记录已选择。

在另外一个窗口中使用“ db2 get snapshot for locks on sample ”命令监控,发现在 RS 隔离级别下,共有 9 个锁:在表上有一个 IS 锁,在读取的 8 行上分别有 1 个 NS 锁。
RR 隔离级别,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
C:\>db2 +c select empno,job,salary from emp1 where job='CLERK' 
for read onlywith RREMPNO JOB   SALARY
 ------ -------- -----------
 000120 CLERK  49250.00
 000230 CLERK  42180.00
 000240 CLERK  48760.00
 000250 CLERK  49180.00
 000260 CLERK  47250.00
 000270 CLERK  37380.00
 200120 CLERK  39250.00
 200240 CLERK  37760.00
  8 条记录已选择。

在另外一个窗口中使用“ db2 get snapshot for locks on sample ”命令监控,发现在 RR 隔离级别下,分为两种情况:
如果该 SQL 语句使用全表扫描,那么即使只读取了 8 行,也会在整个表上加一个 S 锁,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C:\>dynexpln -d sample -q  "select empno,job,salary from emp1 where job='CLERK'
 for read only with rr" – t
 Access Table Name = DB2ADMIN.EMP1  ID = 3,12
 |  #Columns = 2
 |  Relation Scan   -- 注:全表扫描
 |  |  Prefetch: Eligible
 |  Isolation Level: Repeatable Read   -- 注:RR隔离级别
 |  Lock Intents|  |  Table: Share --注:整个表上加S锁
 |  |  Row  : None
 |  Sargable Predicate(s)
 |  |  #Predicates = 1
 |  |  Return Data to Application
 |  |  |  #Columns = 3
 Return Data Completion
 End of section

如果创建索引,并进行索引扫描,那么表上加 IS 锁,读取的每行上加 S 锁。所以对于 RR 隔离级别来说,为了保证并发,尽可能创建合理的索引以减少加锁的范围,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
C:\>db2 create index job on DB2ADMIN.emp1(job)
 DB20000I  SQL 命令成功完成。
 C:\>db2 runstats on table DB2ADMIN.emp1 and indexes all
 DB20000I  RUNSTATS 命令成功完成。
 C:\>dynexpln -d sample -q  "select empno,job,salary from emp1 where job='CLERK'
 for read only with rr" -t
 Access Table Name = DB2ADMIN.EMP1  ID = 3,12
 |  Index Scan:  Name = DB2ADMIN.JOB  ID = 1  --注:索引扫描
 |  |  Regular Index (Not Clustered)
 |  |  Index Columns:
 |  |  |  1: JOB (Ascending)
 |  #Columns = 2
 |  #Key Columns = 1
 |  |  Start Key: Inclusive Value
 | | | | 1: 'CLERK  '
 |  |  Stop Key: Inclusive Value
 | | | | 1: 'CLERK  '
 |  Data Prefetch: Eligible 0
 |  Index Prefetch: None|  Isolation Level: Repeatable Read --注:RR隔离级别
 |  Lock Intents|  |  Table: Intent Share --注:表上加IS锁|  |  Row  : Share --注:行上加S锁
 |  Sargable Predicate(s)
 |  |  Return Data to Application
 |  |  |  #Columns = 3
 Return Data Completion
 End of section

6.1.6  隔离级别摘要

表 6-1 按不期望的结果概述了几个不同的隔离级别。
表 6-1   隔离级别摘要

隔离级别
访问未提交的数据
不可重复读
幻像读现象
可重复读 (RR)
不可能
不可能
不可能
读稳定性 (RS)
不可能
不可能
可能
游标稳定性 (CS)
不可能
可能
可能
未提交读 (UR)
可能
可能
可能

表 6-2 提供了简单的试探方法,以帮助您为应用程序选择初始隔离级别。首先考虑表中列示的方法,并参阅先前对影响各隔离级别因素的讨论,可能会找到另一个更适合的隔离级别。
表 6-2  选择隔离级别的准则

应用程序类型
需要高数据稳定性
需要高数据稳定性
读写事务
RS
CS
只读事务
RR 或 RS
UR

为避免应用程序出现用户无法容忍的现象,必须为其选择适当的隔离级别。在不同隔离级别下,应用程序锁定或释放资源需要不同的 CPU 和内存资源,所以隔离级别不但影响应用程序之间的隔离程度,还可能影响应用程序的个别性能特征。潜在的锁等待情况也会随隔离级别的不同而不同。
因为隔离级别确定访问数据时如何锁定数据并使数据不受其他进程影响,所以您在选择隔离级别时应该平衡并行性和数据完整性需求。您指定的隔离级别在工作单元运行期间生效。
选择正确的隔离级别
使用的隔离级别不仅影响数据库的并发性,而且影响并发应用程序的性能。通常,使用的隔离级别越严格,并发性就越小,某些应用程序的性能可能会随之越低,因为它们要等待资源上的锁被释放。那么,如何决定要使用哪种隔离级别呢?最好的方法是先确定哪些现象是不可接受的,然后选择能够防止这些现象发生的隔离级别。以下列举了各种隔离级别的适用情况:
如果正在执行大型查询,而且不希望并发事务所做的修改导致查询的多次运行返回不同的结果,则使用可重复读隔离级别。
如果希望在应用程序之间获得一定的并发性,还希望限定的行在事务执行期间保持稳定,则使用读稳定性隔离级别。
如果希望获得最大的并发性,同时不希望查询看到未提交的数据,则使用游标稳定性隔离级别。
如果正在只读的表 / 视图 / 数据库上执行查询,或者并不介意查询是否返回未提交的数据,则使用未提交读隔离级别。
设置隔离级别
尽管隔离级别控制事务级上的行为,但实际上它们是在应用程序级被指定的:
对于嵌入式 SQL 应用程序,在预编译时或在将应用程序绑定到数据库 ( 如果使用延迟绑定 ) 时指定隔离级别。在这种情况下,使用 PRECOMPILE 或 BIND 命令的 ISOLATION 选项来设置隔离级别。
对于开放数据库连接 (ODBC) 和调用级接口 (Call Level Interface,CLI) 应用程序,隔离级别是在应用程序运行时通过调用指定了 SQL_ATTR_TXN_ISOLATION 连接属性的 SQLSetConnectAttr() 函数进行设置的。另外,也可以通过指定 DB2CLI.INI 配置文件中的 TXNISOLATION 关键字的值来设置 ODBC/CLI 应用程序的隔离级别;但是,这种方法不够灵活,不能像第一种方法那样为一个应用程序中的不同事务修改隔离级别。
对于 Java 数据库连接 (JDBC) 和 SQLJ 应用程序,隔离级别是在应用程序运行时通过调用 DB2 的 JAVA.SQL 连接接口中的“ setTransactionIsolation() ”方法设置的。
当没有使用这些方法显式指定应用程序的隔离级别时,默认使用游标稳定性 (CS) 隔离级别。这个默认设置被应用于从命令行处理程序 (CLP) 执行的 DB2 命令、SQL 语句和脚本,以及嵌入式 SQL、ODBC/CLI、JDBC 和 SQLJ 应用程序。因此,也可以为从 CLP 执行的操作 ( 以及传递给 DB2 CLP 进行处理的脚本 ) 指定隔离级别。在这种情况下,隔离级别是通过在建立数据库连接之前在 CLP 中执行 CHANGE ISOLATION 命令设置的,输出结果如下:

1
2
3
4
5
6
7
C:\pp>db2 change isolation to ur
 DB21027E  当连接至数据库时未能更改隔离级别。
 C:\pp>db2 connect reset
 DB20000I  SQL 命令成功完成。
 C:\pp>db2 change isolation to ur
 DB21053W  当连接至不支持 UR 的数据库时,会发生自动升级。
 DB20000I  CHANGE ISOLATION 命令成功完成。

在 DB2 V7.1 及更高版本中,能够指定特定查询所用的隔离级别,方法是在 SELECT SQL 语句中加上 WITH [RR | RS | CS | UR] 子句。大家可以看到,本章前面的示例均使用这种方法举例。

6.2  加锁总结

6.2.1  如何获取锁

在大多数情况下,DB2 数据库管理程序在需要锁时隐式地获取它们,因此这些锁在 DB2 数据库管理程序的控制之下。除了使用未提交读隔离级别的情况外,事务从不需要显式地请求锁。实际上,唯一有可能被事务显式锁定的数据库对象是表 (LOCK TABLE) 。图 6-1 说明了用何种逻辑确定为所引用的对象获取什么类型的锁。
图 6-1  如何获取锁
从图 6-1 中我们可以看到,数据库首先判断该 SQL 语句是采用全表扫描还是索引扫描。如果是全表扫描,那么会在整个表上加表级别的锁;如果是读操作,那么获取表级 S 锁;如果是 DML(INSERT、UPDATE 和 DELETE) 操作,那么获取表级 X 锁。假设 SQL 语句采用的是索引扫描,如果是读操作,在读取的行上加 NS 锁,同时在表上加 IS 锁;如果是 DML 操作,那么在操作的行上加 X 锁,同时在表上加 IX 锁。
注意:
假设一个表中有 1000 行数据,某个 SQL 语句访问该表中的两行数据。如果该表没有索引,那么这条 SQL 只能进行全表扫描,这种情况下即使你只访问两行数据,但是由于没有索引也必须进行全表扫描,这时整个表都被加锁。
DB2 数据库管理程序默认总是尝试获取行级锁。但是,可以通过执行特殊形式的 ALTER TABLE 语句来修改这种行为,输出结果如下:

1
ALTER TABLE [TableName] LOCKSIZE TABLE

其中的 TableName 标识一个现有表的名称,所有事务在访问它时都要获取表级锁。 ALTER TABLE 语句的 LOCKSIZE 子句指定行级别或表级别的锁定作用域 ( 详细程度 ) 。默认情况下,使用行锁定。这些已定义的表锁定仅请求 S( 共享 ) 和 X( 互斥 ) 锁定。 ALTER TABLE 语句的 LOCKSIZE ROW 子句不会阻止正常的锁定升级。
也可以在应用程序中通过执行 LOCK TABLE 语句,强制 DB2 数据库管理程序为特定事务在表上获取表级锁,输出结果如下:

1
LOCK TABLE [TableName] IN [SHARE | EXCLUSIVE] MODE

其中的 TableName 标识一个现有表的名称,对于这个表应该获取表级锁 ( 假定其他事务在该表上没有不兼容的锁 ) 。如果在执行这个语句时指定了共享 (SHARE) 模式,就会获得一个允许其他事务读取 ( 但不能更改 ) 表中数据的表级锁;如果执行时指定了互斥 (EXCLUSIVE) 模式,就会获得一个不允许其他事务读取或修改表中数据的表级锁。
在下列情况下,由 ALTER TABLE 语句定义的永久表锁定可能比使用 LOCK TABLE 语句获得的单个事务表锁定更可取,原因如下:
表是只读的,且将始终只需要 S 锁定,其他用户也可以获取表的 S 锁定。
表通常由只读应用程序访问,但有时由单个用户访问可以进行简要维护,而该用户需要 X 锁定。当维护程序运行时,将只读应用程序锁定在外,但在其他情况下,只读应用程序可以使用最小的锁定开销同时访问表。
总结一下:ALTER TABLE语句全局指定锁定,它影响访问该表的所有应用程序和用户。单个应用程序可以使用LOCK TABLE语句来指定应用程序级别的表锁定。

6.2.2  意图锁和非意图锁

对于 IN、IX、IS 和 SIX 这些意图 (INTENT) 锁,读者可以这样理解:严格来说它们并不是一种锁,而是用来存放表中行锁的信息。举个通俗的例子,我们去住一个酒店。我们把整个酒店比喻成一张表,每个房间是一行。那么当我们预订一个房间时,就对该行 ( 房间 ) 加 X 锁,但是同时会在酒店的前台对该行 ( 房间 ) 做一个信息登记 ( 旅客姓名、身份证、住多长时间等 ) 。大家可以把意图锁当成是这个酒店前台的登记信息,它并不是真正意义上的锁,而是维护表中每行的加锁情况,所有访问这个表的应用程序共用这个意图锁。后续的旅客来时通过酒店前台来看哪个房间是可住的。那么如果没有意图锁,会出现什么情况呢?假设我要预订房间,那么每次我都需要到每一个房间查看确认这个房间有没有住旅客,这样的效率显然是很低下的。其实最早的 DB2 版本是没有意图锁的,但是这对并发影响非常大,后来就增加了意图锁。所有的数据库 (Oracle、Informix 和 Sybase) 都有意图锁的实现机制。在一个表上只有一个意图锁,所有应用程序共用这个意图锁,但是可能经常会更改。

6.2.3  读锁和写锁

在 DB2 数据库中有两种主要类型的锁:读锁 (S) 和写 (X) 锁。
一般来说读锁是在如下情况下加的:
NS 是在 RS 和 CS 隔离级别下对读取到的行加的锁。而 S 锁是在 RR 隔离级别下对读取到的表 ( 使用全表扫描 ) 或行 ( 使用索引扫描 ) 加的锁。 U 锁是在“ select * from t1 for update ”情况下加的锁。这些锁都是在读取 (SELECT) 期间加的锁。
一般来说写锁是在如下情况下加的:
Z 锁是超级排它锁,它不允许任何隔离级别的读取,一般是在数据物理结构发生改变的情况下加的锁。例如:CREATE、ALTER、DROP、离线 REORG 和离线 LOAD 期间会加 Z 锁。 X 锁是在做 INSERT、UPDATE 和 DELETE 期间加的锁,它允许使用 UR 隔离级别进行未提交读取。 NW 锁表示当一行被插入到索引中的时候,该行的下一行会被加上该锁。锁的拥有者可以读但不能更改锁定行。该锁与 X 锁类似,只是与 NS 锁兼容。

6.2.4  LRB(Lock Resource Block)

每个数据库都有一个锁列表,该列表包含所有同时连接到数据库的应用程序所持有的锁。在 32 位平台上,一个对象上的第一个锁要求占 72 字节,而其他锁要求占 36 字节。在 64 位平台上,第一个锁要求占 128 字节 (HP 平台为 80 字节 ),而其他锁要求占 64 字节。 关于锁占用资源块 (LRB:Lock Resource Block),在各个版本还不一样,表 6-3 是 DB2 V9 中 LRB 占用资源的情况。
表 6-3  DB2 V9 中 LRB 占用资源的情况

Architecture
LRB Size
First Transaction to Lock
Subsequent Locks
32-bit
48 bytes
96 bytes
48 bytes
64-bit
64 bytes
128 bytes
64 bytes
64-bit HP_UX
80 bytes
160 bytes
80 bytes

注意:
关于 LRB,在 DB2 的各个版本很不一样。在 DB2 V8 之前,在 32 位平台上,在一个没有持有其他锁定的对象上持有一个锁定需要 72 字节,在一个持有了现存锁定的对象上记录一个锁定需要 36 字节;在 DB2 V8 的后期版本中,在一个没有持有其他锁定的对象上持有一个锁定需要 64 字节,在一个持有了现存锁定的对象上记录一个锁定需要 32 字节;在 64 位平台上,要对没有其他锁定的对象上保留锁定需要 112 字节,要对具有现有锁定的对象上保留锁定需要 56 字节。 DB2 V9 中的 LRB 情况如表 6-3 所示。

6.2.5  USE AND KEEP LOCKS

在 DB2 中,默认情况下锁都是由 DB2 数据库管理器根据应用程序的隔离级别自动设置锁类型。 DB2 提供了一种方式允许用户明确地向 DB2 数据库管理器请求锁类型:
USE AND KEEP EXCLUSIVE LOCKS:向 DB2 数据库管理器明确请求在数据上加排它锁。
USE AND KEEP UPDATE LOCKS:向 DB2 数据库管理器明确请求在数据上加更新锁。
USE AND KEEP SHARE LOCKS:向 DB2 数据库管理器明确请求在数据上加共享锁。
例如:

1
2
DECLARE c1 CURSOR FOR select empno,job,salary from emp
where job='CLERK' FOR UPDATE WITH RS USE AND KEEP EXCLUSIVE LOCKS

在上面的语句中,如果没有带 USE AND KEEP EXCLUSIVE LOCKS 子句,默认情况下 DB2 会向行加更新锁 (U 锁 ),使用了该子句后将会变为排它锁 (X 锁 ) 。
再如:

1
2
DECLARE c1 CURSOR FOR select empno,job,salary from emp
where job='CLERK' FOR FETCH ONLY WITH RR USE AND KEEP UPDATE LOCKS

在上面的语句中,如果没有带 USE AND KEEP EXCLUSIVE LOCKS 子句,默认情况下 DB2 会向行加下一键共享锁 (NS 锁 ),使用了该子句后将会变为更新锁 (U 锁 ) 。
再如:

1
2
DECLARE c1 CURSOR FOR select empno,job,salary from emp
where job='CLERK' FOR UPDATE WITH RS USE  AND  KEEP SHARE LOCKS

在上面的语句中,如果没有带 USE AND KEEP EXCLUSIVE LOCKS 子句,默认情况下 DB2 会向行加更新锁 (U 锁 ),使用了该子句后将会变为下一键共享锁 (NS 锁 ) 。
为什么要这样显式请求锁类型呢?这是因为 USE AND KEEP LOCKS 显式请求锁类型有助于避免多个存取数据库的独立进程的应用程序可能产生的死锁。例如,在一个应用程序中的数个进程存取同一个表,对该表并行进行读取及写入操作。如果这些进程执行读 SQL 查询,然后再对同一表执行 SQL 更新,那么各个进程间对同一数据潜在的争用会使得死锁的几率增大。例如,如果两个进程读该表,然后更新该表,那么 A 进程先获得 S 锁,同时 B 进程也获得 S 锁。当 A 进程发出更新语句时试图获得对行的 X 锁定,而 B 进程对该行具有 S 锁定。此时 A 进程进入锁等待 (Lock Wait) 状态,等待 B 进程释放 S 锁。当后来进程发出更新语句时试图获得对行的 X 锁定,而进程 A 对该行具有 S 锁定。此时 B 进程进入锁等待 (LOCK WAIT) 状态,等待 A 进程释放 S 锁。这样就产生了死锁,为了避免发生这种死锁,存取具有修改意向的数据的应用程序应该执行下列其中一项操作:
执行选择操作时使用 FOR UPDATE OF 子句。此子句确保当 A 进程试图读取该数据时进行 U 锁定,禁用行分块 (BLOCKING) 。
执行查询时使用 WITH RR USE AND KEEP UPDATE LOCKS 子句或 WITH RS USE AND KEEP UPDATE LOCKS 子句。任一子句都确保当 A 进程试图读取该数据时进行 U 锁定,并且允许行分块 (BLOCKING) 。

6.2.6  索引类型和下一键锁

DB2 中有两种索引类型:type-1 索引和 type-2 索引。这两种索引加锁的情况是不一样的,下面我们来分别介绍这两种索引的加锁算法。
type-1 索引加锁算法
在 DB2 V8 之前,DB2 只有一种索引类型,也就是我们今天称之为的 TYPE-1 索引,这种索引在删除和插入的时候特别容易引起死锁从而影响并发。是什么原因呢,下面我们举一个使用 TYPE-1 索引的例子:
假设一个索引的叶子 (LEAF) 中包含 1、5、6、7、8、12 6 个 KEY 。
假如现在交易 1 删除 KEY VALUE 8 对应的行,在删除期间,KEY VALUE 8 对应的行上会加 X 锁。当 KEY VALUE 8 被删除以后,就会在索引的下一键也就是 8 的下一个键 12 上加 NX 锁,相应地会在 KEY VALUE 12 对应的行上加 X 锁。
如果另外一个交易 2 删除 KEY VALUE 5 对应的行,在删除期间,KEY VALUE 5 对应的行上会加 X 锁。当 KEY VALUE 5 被删除以后,就会在索引的下一键也就是 5 的下一个键 6 上加 NX 锁,相应地会在 KEY VALUE 6 对应的行上加 X 锁。
假设现在交易 1 插入一行 KEY VALUE 4 相应的行,这一行会加 W 锁,当插入新的 KEY 到索引的时候,KEY VALUE 6 对应的行会加 NW 锁。因为此时交易 2 对应的行上持有 X 锁,这时它不得不等待交易 2 释放掉该锁。
同样假设现在交易 2 插入一行 KEY VALUE 9 相应的行,这一行会加 W 锁,当插入新的 KEY 到索引的时候,KEY VALUE 12 对应的行会加 NW 锁。因为此时交易 1 对应的行上持有 X 锁,这时它不得不等待交易 1 释放掉该锁。
所以在 type-1 索引时,因为更改期间加锁的方式是 W 和 NW,所以很容易造成死锁,这会极大影响数据库的并发。
type-2 索引加锁算法
DB2 从 V8 以后所有新创建的索引都是 type-2 类型的索引。 type-2 索引可以大大減少 NEXT KEY 锁从而改进了性能,因为各项是标记为删除的而不是从页面中物理删除的 ( 伪删除 ) 。 type-2 索引同时允许索引列大于默认的 255 字节,同时还可以在线运行 REORG 和 RUNSTATS,并且支持新的多维群集 (MDC) 功能。在 DB2 V8 中,所有新的索引都是以 type-2 类型创建的,只有已经在表上定义了 ( 迁移前 ) type-1 索引的時候除外。可以使用 REORG INDEXES 将 type-1 索引转换为 type-2 索引。 Type-2 索引采用的是伪删除算法,图 6-2 是两种索引类型加锁的比较。
图 6-2  type-1 和 type-2 索引加锁比较
在 DB2 V8 之前的版本中,插入过程中可能使用 W 或 NW 锁,但是在 DB2 V8 以后只有在使用了隔离级别为 RR 的情况下才会出现这两种锁。因此,应尽可能避免这种情况。

6.2.7  扫描方式加锁情况

在 DB2 数据库中,不同的扫描方式在不同的隔离级别下加锁的情况也是不一样的,在 DB2 中主要有以下几种扫描方式:全表扫描、索引扫描和 RID 扫描 ( 注:如果使用了 MDC,那么还会有其他扫描方式,但此处我们不讨论 ) 。其中 RID 扫描也算是索引扫描。表 6-5 和表 6-6 总结了在全表和索引扫描方式下加锁的情况。
表 6-4  表扫描时在表 / 行上加锁情况
表 6-5  索引扫描时在表 / 行上的加锁情况
表 6-5  索引扫描时在表 / 行上的加锁情况
关于在不同隔离级别和扫描方式下表 / 行上的加锁情况,我们可以在 DB2 解释工具的输出中查看加锁情况。下面我们分别对全表扫描和索引扫描的加锁情况举例:
全表扫描加锁情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
dynexpln – d sample – q "select * from employee with rr" -t
 ---------------------------- 略 --------------------------
 |  Isolation Level: Repeatable Read
 |  Lock Intents
 |  |  Table: Share
 |  |  Row  : None
 |  Sargable Predicate(s)
 ---------------------------- 略 ------------------------------
 dynexpln – d sample – q "select * from employee with rs" -t|  Relation Scan --注:全表扫描
|  |  Prefetch: Eligible|  Isolation Level: Read Stability --注:隔离级别为RS
|  Lock Intents|  |  Table: Intent Share --注:表上加IS锁
 |  |  Row  : Next Key Share--注:行上加 NS 锁
 |  Sargable Predicate(s)

索引扫描加锁情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dynexpln – d sample – q "select * from employee where empno='000100' with rs" -t
 ---------------------------- 略 --------------------------
 |  Index Scan:  Name = ORACLE.PK_EMPLOYEE  ID = 1
 |  |  Regular Index (Not Clustered)
 |  |  Index Columns:
 |  |  |  1: EMPNO (Ascending)
 |  #Columns = 13
 |  Volatile Cardinality
 |  Single Record
 |  Fully Qualified Unique Key
 |  #Key Columns = 1
 |  |  Start Key: Inclusive Value
 |  |  |  |  1: '000100'
 |  |  Stop Key: Inclusive Value
 |  |  |  |  1: '000100'
 |  Data Prefetch: None
 |  Index Prefetch: None|  Isolation Level: Read Stability --注:隔离级别为RS
|  Lock Intents|  |  Table: Intent Share --注:表上加IS锁
|  |  Row  : Next Key Share --注:行上加NS锁

通过上面两个例子,我们希望读者能够明白关于扫描方式和隔离级别的加锁情况,关键是如何分析加锁情况。

6.3  乐观锁

6.3.1  悲观锁定和乐观锁定

悲观锁定
DB2 默认情况下都是采用悲观锁定方式工作,本节上面讲述的各个例子都属于这种情况。
悲观锁定策略的前提是,另一个用户很可能试图修改您正在修改的某个表行。如果选择了某个行,并检测到其他用户试图对该行执行更新或删除操作,那么该行将在这段时间内持有锁 ( 例如,通过使用 RR 隔离级别或以排它锁 (X) 模式锁定表 ) 。悲观锁定的优点就是能够保证实现一致且安全的更改。但是这种锁定策略的主要缺点就是并发性较差。对于具有大量用户或运行长期事务的系统,或者涉及大量 SQL 操作的事务,需要等待锁释放的概率则会增加。
图 6-3 阐释了悲观锁定的功效。事务 1 读取某条特定记录并对该行应用一个锁,并且需要花些时间确定是否要对这个行执行更新操作。同时,事务 2 希望访问这个行,但是它必须等待事务 1 释放该行的锁。也就是说,只有事务 1 释放锁后,事务 2 才能收到SELECT操作的结果并继续执行它的业务逻辑。
图 6-3   悲观锁定的工作示意图
乐观锁定
悲观锁定方法的主要问题是事务之间必须互相等待。避免发生这种情况的方法就是使用乐观锁定策略,即假设在修改某行时,另一个用户试图对这一行进行修改的可能性极低。如果确实对这一行进行了修改,那么更新或删除操作将会失败,应用程序逻辑将处理这些失败,例如重新尝试选择。通过使用这种方法,使得事务在对行执行选择、更新或删除操作期间,不会持有行锁。但是,由此产生的问题是需要一种方式确保数据在被读取和修改期间没有发生变化。尽管应用程序需要更多的重试逻辑,但乐观锁定策略的主要优点是通过最小化给定资源对其他事务的不可用时间,来减少锁竞争,同时不会牺牲数据完整性 ( 虽然乐观锁定刚刚被引入 DB2 FOR Linux/UNIX/Windows,但是早已被提供给 DB2 FOR z/OS 用户 ) 。因此,它具有比悲观锁定更好的伸缩性。
图 6-4 阐释了乐观锁定背后的思想。与图 6-3 类似,事务 1 读取某个特定记录,但随后即释放锁。因此,事务 2 现在可以顺利地对同一行进行检索。在提交事务之前,事务 1 和事务 2 都必须检查该行在执行前面的SELECT之后是否发生改变。如果发生了一处修改,事务就必须重新执行新的SELECT来检索当前数据。然而,若该行在执行SELECT之后未发生变化,则可以成功更新数据。
图 6-4  乐观锁定的工作示意图

6.3.2  DB2 V9.5 的乐观锁定

DB2 V9.5 的乐观锁定特性最小化了给定资源对于其他事务的不可用时间,进一步改善了并发性。由于数据库管理器能够确定某一行何时会被修改,因此可以保证数据完整性,同时限制持有锁的时间。通过实现乐观并发控制,数据库管理器可以在完成读操作后立即释放行或页锁。
DB2 V9.5 支持的乐观锁定特性简便、快捷,并且不会产生误判 (False Positive) 。这一特性通过如下所示的新 SQL 函数、表达式和特性来实现:
行标识符(RID_BITRID)内置函数:该内置函数可用于SELECT列表或谓词语句。例如,在谓词“WHERE RID_BIT(tab)=? ”中,RID_BIT等于谓词被实现为一种新的直接访问方法,从而可以更有效地定位行。在 DB2 V9.5 以前,这种称为值乐观锁定的技术确定值的方式为:将所有选择的列值添加到谓词,然后应用某些唯一的列组合来筛选出单个行,这种访问方法效率较低。
ROW CHANGE TOKEN表达式:这种新的表达式返回一个标记作为 BIGINT 。这个标记表示某一行的修改序列中的一个相对点。应用程序可以将某行的当前行修改标记值与上次取回行时保存的行修改标记值进行比较,以判断行是否发生修改。
基于时间的更新检测:这个特性通过ROW CHANGE TIMESTAMP表达式添加到 SQL 中。要支持这一特性,表需要定义一个新生成的行修改时间戳列来保存时间戳值。这可以通过ALTER TABLE语句添加到现有表,或者在创建新表时定义行修改时间戳列。是否提供行修改时间戳列还将影响乐观锁定的行为,因为该列有助于将行修改标记的粒度从页级别提高到行级别,这对乐观锁定应用程序非常有利。
隐式隐藏列:从兼容性方面来说,这个特性有助于将行修改时间戳列应用到现有表和应用程序。在使用隐式列列表时,隐式隐藏列不会被外部化。例如,对表执行SELECT* 时不会在结果表中返回隐式隐藏的列,并且执行不包含列列表的INSERT语句时也不会要求提供隐式隐藏列的值,但是隐式隐藏列必须定义为允许 NULL 值或具有另一个默认值。
使用上述编程模型的应用程序将从增强的乐观锁定特性中获益。注意,未使用这种编程模型的应用程序被认为是非乐观锁定应用程序,它们将按照以前的方式工作。
图 6-5 阐释了 DB2 V9.5 的乐观锁定特性的功效。事务 1 和事务 2 同时读取相同的行,包括 RID_BIT 和 ROW CHANGE TOKEN 值。随后,事务 1 在执行完SELECT并确保该行未发生修改后,通过将RID_BIT 和 ROW CHANGE TOKEN谓词添加到UPDATE语句对行进行更新。现在,当事务 2 尝试使用与事务 1 相同的谓词对同一行进行更新时,它无法查找到该行,因为ROW CHANGE TOKEN的值已经根据事务 1 的UPDATE进行了更改。事务 2 必须进行重试更新,以取回最新的数据。
图 6-5  DB2 V9.5 的乐观锁定特性
启用乐观锁定特性
由于不需要对表进行 DDL 修改即可使用针对乐观锁定的新 SQL 表达式和属性,因此可以轻松地在您的测试应用程序中尝试乐观锁定特性。
注意,在不修改表结构增加 ROW CHANGE TIMESTAMP 列的情况下,乐观锁定应用程序可能会产生更多的误判 (False Positive) 。在生产环境中,如果应用程序发生了误判 (False Positive),就不能够实现较好的并发,因为误判很可能造成大量重试操作。因此,要避免发生误判,执行乐观锁定的目标表应执行以下任意一种 DDL 操作:
创建时定义 ROW CHANGE TIMESTAMP 列
修改以增加 ROW CHANGE TIMESTAMP 列
当在应用程序中希望直接启用乐观锁定支持而不愿意修改表结构时,只需要执行以下基本步骤即可:
(1) 在初始查询中,对要进行处理的所有行的行标识符和行修改标记执行SELECT( 使用RID_BIT() 和 RID()内置函数 ) 。
(2) 释放行锁,以便其他应用程序可以对表执行SELECT、INSERT、UPDATE 和 DELETE( 例如,使用游标稳定性 (CS) 隔离级别或未提交读 (UR) 隔离级别 ) 。
(3) 对目标行执行可搜索的UPDATEDELETE,在搜索条件中使用行标识符和行修改标记,乐观地假定自从执行最初的SELECT语句后,未锁定的行没有发生过修改。
(4) 如果行发生了修改,UPDATE操作将失败,应用程序逻辑必须处理这一失败。例如,应用程序将重试SELECT 和 UPDATE操作。
运行以上步骤之后,如果存在下述情况,则可以考虑更新表结构增加 ROW CHANGE TIMESTAMP 列:
如果应用程序执行重试的次数超过预期值或与预期值相同,那么向表添加一个 ROW CHANGE TIMESTAMP 列,以确保RID_BIT函数对行标识符作出的修改只会使行修改标记无效,而同一数据页上的其他活动不受影响。
要查看给定时间范围内执行了插入或更新操作的行,需要创建或修改表以包含一个 ROW CHANGE TIMESTAMP 列。该列由数据库管理器自动维护,并可以通过列名或ROW CHANGE TIMESTAMP表达式进行查询。
对于 ROW CHANGE TIMESTAMP 列 ( 只针对这种列 ),如果该列使用IMPLICITLY HIDDEN属性定义,那么当对表列进行隐式引用时,不会对该列执行外部化。然而,在 SQL 语句中,可以始终显式引用一个隐式隐藏的列。当向表添加列可能造成使用隐式列列表的应用程序失败时,这一特性非常有用。
行修改标记的粒度和误判
RID_BIT()内置函数和行修改标记是实现乐观锁定的唯一需求。然后使用 ROW CHANGE TIMESTAMP 列会促使 DB2 服务器保存最后一次修改 ( 或第一次插入 ) 行的时间。这提供了一种方式捕获最近一次修改行的时间戳。可以通过以下任意一条语句定义行修改时间戳列:
GENERATED ALWAYSFOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP
该时间戳列始终由数据库管理器维护。
GENERATED BY DEFAULTFOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP
该时间戳列默认情况下由数据库管理器维护,但是也接受用户提供的输入值。
当应用程序对表使用新的 ROW CHANGE TOKEN 表达式时,需要考虑以下两种可能性:
表没有定义行修改时间戳列:ROW CHANGE TOKEN 表达式返回一个派生的 BIGINT 值,由同一页面中的所有行共享。如果页面中的某行被更新,那么将针对该页面中的所有行修改行修改标记。这意味着对其他行进行修改时更新会失败,这一特性被称为误判。
注意:
只有在应用程序可以容忍误判,并且不希望向每一行添加针对 ROW CHANGE TIMESTAMP 列的额外存储的情况下,才使用这种模式。
表具有一个行修改时间戳列:ROW CHANGE TOKEN 表达式返回一个从列的时间戳值获得的 BIGINT 值。在这种情况下,发生误判的几率大大减少。如果对表进行了重组和重新分布,那么当移动某行并且应用程序使用以前的RID_BIT()值时,将发生误判现象。
可通过以下SELECT 语句检查行修改时间戳列是否存在,SELECT 查询输出如下所示:

1
2
3
4
5
SELECT COLNAME, ROWCHANGETIMESTAMP, GENERATED FROM SYSCAT.COLUMNS
 WHERE TABNAME='tablename' AND ROWCHANGETIMESTAMP='Y'
  COLNAME ROWCHANGETIMESTAMP  GENERATED
  ------------ ------------------  ---------
  ROWCHGTS Y      A

在上述输出中,存在一个行修改时间戳列 ROWCHGTS,并通过 GENERATED ALWAYS 子句定义 ( 值“ A ”表示 GENERATED ALWAYS,而值“ D ”表示 GENERATED BY DEFAULT) 。
基于时间的更新检测
某些应用程序需要了解特定时间范围内的数据库更新,以便进行数据复制、场景审计等等。这可以通过包含行修改时间戳列的表实现,通过定义行修改时间戳列来保存ROW CHANGE TIMESTAMP表达式生成的时间戳值。这种新的ROW CHANGE TIMESTAMP表达式返回的时间戳表示最后一次进行行修改的时间,使用类似于CURRENT TIMESTAMP的本地时间表示。对于已经更新过的行,将反映对行执行的最近更新。否则,该值将对应于最初的行插入时间。
ALTER TABLE语句之后未进行更新的行将返回列的类型默认值,该值为 0001 年 1 月 1 日午夜。只有进行过更新的行才具有唯一的时间戳。使用离线表重组对时间戳进行具体化的行将返回一个唯一的时间戳,该时间戳在表重组期间生成。REORG仅仅使用INPLACE选项无法满足需求,因为它没有对模式修改进行具体化。 下面是一些基于时间的更新检测示例,具有行修改时间戳列的表如下所示:

1
2
3
4
5
6
CREATE TABLE EMPLOYEE (EMPNO CHAR(6) NOT NULL,
  ......
  ROWCHGTS TIMESTAMP NOT NULL
 GENERATED ALWAYS
  FOR EACH ROW ON UPDATE AS
  ROW CHANGE TIMESTAMP)

对于未定义行修改时间戳列的表,可稍后通过下面这条 ALTER TABLE 语句添加,如下所示:

1
2
3
4
5
ALTER TABLE EMPLOYEE ADD COLUMN
  ROWCHGTS TIMESTAMP NOT NULL
  GENERATED ALWAYS
  FOR EACH ROW ON UPDATE AS
  ROW CHANGE TIMESTAMP

选择在最近 30 天内发生修改的所有行的 SQL 语句如下所示:

1
2
3
SELECT * FROM EMPLOYEE WHERE
  ROW CHANGE TIMESTAMP FOR EMPLOYEE <= CURRENT TIMESTAMP AND
  ROW CHANGE TIMESTAMP FOR EMPLOYEE >= CURRENT TIMESTAMP - 30 days

表 6-6 展示了在创建具有行修改时间戳列的表后,使用 INSERT、IMPORT 或 LOAD 填充后的 ROW CHANGE TIMESTAMP 列的内容。
表 6-6  INSERT、IMPORT 或 LOAD 填充后的 ROW CHANGE TIMESTAMP 列的内容

EMPNO
FIRSTNME
LASTNAME
PHONENO
ROW CHANGE TIMESTAMP
000010
CHRISTINE
HAAS
3978
2008-12-20 13:53:01.296000
000030
SALLY
KWAN
4738
2008-12-20 13:53:01.312001

表 6-7 展示了将行修改时间戳列添加到现有表后,ROW CHANGE TIMESTAMP 列的内容。
表 6-7  行修改时间戳列添加到现有表后,ROW CHANGE TIMESTAMP 列的内容

EMPNO
FIRSTNME
LASTNAME
PHONENO
ROW CHANGE TIMESTAMP
000010
CHRISTINE
HAAS
3978
0001-01-01 00:00:00.000000
000030
SALLY
KWAN
4738
0001-01-01 00:00:00.000000

隐式隐藏列
这一特性有利于将行修改时间戳列应用到现有表和应用程序中。CREATEALTER TABLE语句中的IMPLICITLY HIDDEN属性表示:除非根据名称显式引用列,否则该列在 SQL 语句中不可见。例如,假设某个表包含一个使用IMPLICITLY HIDDEN子句定义的列,SELECT *操作的结果将不会包含隐式隐藏的列。然而,如果SELECT显式引用隐式隐藏列的名称,那么它将在结果表中包含该列。只有 ROW CHANGE TIMESTAMP 列才能使用IMPLICITLY HIDDEN 属性。行修改时间戳的隐式隐藏列的声明方法如下:
CREATE TABLE SALARY_INFO (

1
2
3
4
5
6
7
8
9
10
LEVEL INT NOT NULL,
  SALARY INT NOT NULL,
  UPDATE_TIME TIMESTAMP NOT NULL
  IMPLICITLY HIDDEN
  GENERATED ALWAYS FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP)
  
  ALTER TABLE SALARY_INFO
  ADD COLUMN UPDATE_TIME TIMESTAMP NOT NULL
  IMPLICITLY HIDDEN
  GENERATED ALWAYS FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP

使用 DESCRIBE 命令显示表列的结果输出,如下所示:

1
2
3
4
5
6
7
DESCRIBE TABLE SALARY_INFO
  Data type  Column
  Column name  schema  Data type name  Length    Scale   Nulls
  -----------   --------- -------------------   ---------- ----- ------
  LEVEL  SYSIBM  INTEGER  4  0    No
  SALARY  SYSIBM  INTEGER  4  0    No
  UPDATE_TIME  SYSIBM  TIMESTAMP  10  0   No

针对隐式隐藏的列执行 INSERT 和 SELECT 操作,如下所示:

1
2
3
4
5
INSERT INTO SALARY_INFO VALUES (1, 50000)
  SELECT * FROM SALARY_INFO
  LEVEL   SALARY
  ----------- -----------
  1   50000

可以通过下面的 SQL 语句显式引用隐式隐藏列的 INSERT 和 SELECT:

1
2
3
4
5
6
INSERT INTO SALARY_INFO (LEVEL, SALARY, UPDATE_TIME)
  VALUES (2, 30000, DEFAULT)
 SELECT LEVEL, SALARY, UPDATE_TIME FROM SALARY_INFO  WHERE LEVEL = 2
  LEVEL  SALARY  UPDATE_TIME
  -----------   ----------- --------------------------
 2  30000   2008-12-18-15.34.24.437000

乐观锁定特性的局限性和注意事项
不能指定为 ROW CHANGE TIMESTAMP 的列包括:主键、外键、多维聚合 (MDC) 列、范围分区 (RANGE PARTITION) 列、数据库哈希分区键、DETERMINED BY 约束列和别名。
DPF 配置不支持RID()函数。
在乐观锁定场景中,在取回到更新操作期间执行在线或离线表REORG可能会造成更新失败,但是普通的应用程序重试逻辑应该能够处理。
在 DB2 V9.5 中,IMPLICITLY HIDDEN属性只能应用于 ROW CHANGE TIMESTAMP 列以实现乐观锁定。
对于后来才添加 ROW CHANGE TIMESTAMP 列的表,在保证所有行已被具体化之前,INPLACE REORG的使用会受到限制 ( 返回错误代码 SQL2219, REASON CODE=13) 。这可通过LOAD REPLACE命令或典型的表REORG实现。这将防止发生误判,具有 ROW CHANGE TIMESTAMP 列的表没有限制。

6.3.3  乐观锁应用案例

上面我们讲解了乐观锁的原理和实现技术,现在我们来举一个乐观锁的应用案例。假设某员工履行新的工作职责并被调到另一个部门工作。公司的两名经理 ( 原部门的经理 MANAGER1 和新部门的经理 MANAGER2) 正在使用人事管理应用程序更新 SAMPLE 数据库的 EMPLOYEE 表中的员工记录。此时存在一种可能,即两名经理可能同时对该员工的记录进行更新。当 MANAGER1 选择并更新这条员工记录时,MANAGER2 也对同一条记录执行更新。这时,乐观锁定特性将发挥作用,例如,它使 MANAGER2 在使用当前应用程序更新时了解到某个特定记录已经被更新过。这样,应用程序可以更容易地执行指令,因为它不必实现自己的更新检测逻辑。下面我们讲述几个常见的应用场景:
场景 1
EMPLOYEE 表包含一个隐式隐藏的 ROW CHANGE TIMESTAMP 列 ( 后来添加的 ),并且只有 MANAGER1 访问了该表。 MANAGER1 从 EMPLOYEE 表中选择数据,并在稍后尝试将“ Christine Haas ”的电话号码 3978 更新为 1092 。更新成功。这个场景的主要步骤如下:
(1) MANAGER1 执行下面的 SELECT 语句:
SELECT RID_BIT(EMPLOYEE),

1
2
3
ROW CHANGE TOKEN FOR EMPLOYEE,
  EMPNO, FIRSTNME, LASTNAME, PHONENO, ROWCHGTS
  FROM EMPLOYEE FETCH FIRST 3 ROWS ONLY

(2) SELECT 的输出结果如下:
乐观锁定表达式 EMPLOYEE 表

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN  EMPNO  FIRSTNME  LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 74904229642240  000010  CHRISTINE 
HAAS  3978  0001-01-01 00:00:00.000000
 x'05004001000000000000000000FA9023' 74904229642240  000020  MICHAEL
THOMPSON  3476  0001-01-01 00:00:00.000000
 x'06004001000000000000000000FA9023' 74904229642240  000030  SALLY 
KWAN  4738  0001-01-01 00:00:00.000000

(3) MANAGER1 执行下面的 UPDATE 语句:
UPDATE EMPLOYEE SET

1
2
3
(FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
  WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
  ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

(4) UPDATE 的输出结果如下:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 141285645885181032 000010 CHRISTINE
HAAS 1092  2008-12-20 11:55:45.593000
 x'05004001000000000000000000FA9023' 74904229642240 000020 MICHAEL
THOMPSON 3476  0001-01-01 00:00:00.000000
 x'06004001000000000000000000FA9023' 74904229642240 000030 SALLY
KWAN 4738  0001-01-01 00:00:00.000000

在上面的这个场景中,我们可以看到当MANAGER1执行UPDATE操作后,EMPLOYEE表中 ROW CHANGE TIMESTAMP 字段从默认的“ 0001-01-01:00:00:00.000000 ”变为“ 2008-12-20:11:55:45.59 ”,也就是进行更改的那个时刻的时间戳。
场景 2
EMPLOYEE 表包含一个隐式隐藏的 ROW CHANGE TIMESTAMP 列,并且 MANAGER1 和 MANAGER2 同时访问该表。 MANAGER1 从 EMPLOYEE 表中选择数据并稍后尝试更新这些数据。然而,从他选择这些数据到执行更新期间,MANAGER2 对相同的数据进行了更新。 MANAGER2 执行的更新成功,而 MANAGER1 执行的更新失败。主要步骤如下:
(1) MANAGER1 和 MANAGER2 执行 SELECT 的输出结果如下所示:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 74904229642240 000010 CHRISTINE
HAAS 3978  0001-01-01 00:00:00.000000
 x'05004001000000000000000000FA9023' 74904229642240 000020 MICHAEL
THOMPSON 3476  0001-01-01 00:00:00.000000
 x'06004001000000000000000000FA9023' 74904229642240 000030 SALLY
KWAN 4738  0001-01-01 00:00:00.000000

(2) MANAGER2 执行下面的 UPDATE 语句:

1
2
3
4
UPDATE EMPLOYEE SET
  (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
  WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
  ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

(3) MANAGER2 执行 UPDATE 的输出结果如下:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 141285645885181032 000010 CHRISTINE
HAAS 1092  2008-12-20 11:55:45.593000
 x'05004001000000000000000000FA9023' 74904229642240 000020 MICHAEL
THOMPSON 3476  0001-01-01 00:00:00.000000
 x'06004001000000000000000000FA9023' 74904229642240 000030 SALLY
KWAN 4738  0001-01-01 00:00:00.000000

(4) MANAGER1 执行下面 UPDATE 语句:

1
2
3
4
UPDATE EMPLOYEE SET
  (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
  WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
  ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

(5) MANAGER1 执行 UPDATE 失败,返回 SQL0100W 信息。
MANAGER1 的更新失败。由于 MANAGER2 执行了UPDATE,ROW CHANGE TOKEN 发生了改变,因此,当将执行SELECT时取回的标记与由 MANAGER2 的应用程序更新后的当前值进行比较时,MANAGER1 的 UPDATE 语句的 ROW CHANGE TOKEN 谓词失败。因此UPDATE无法找到指定的行,返回消息“SQL0100W No row was found for FETCH, UPDATE or DELETE; or the result of a query is an empty table. SQLSTATE=02000 ”
场景 3
EMPLOYEE 表包含一个隐式隐藏的 ROW CHANGE TIMESTAMP 列,并且 MANAGER1 和 MANAGER2 同时访问该表。 MANAGER1 对行进行了更新,但还未提交修改。 MANAGER2 使用 UR 隔离级别从 EMPLOYEE 表中选择数据。 MANAGER1 提交他做出的修改。 MANAGER2 尝试对相同的数据进行更新。 MANAGER2 执行更新成功,因为应用程序读取的是 MANAGER1 未提交的更新。然而,如果 MANAGER1 回滚更新而不是提交更新,MANAGER2 的更新将失败。这个场景的主要步骤如下:
(1) MANAGER1 执行 SELECT 的输出结果如下:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 74904229642240 000010 CHRISTINE
HAAS 3978  0001-01-01 00:00:00.000000
 x'05004001000000000000000000FA9023' 74904229642240 000020 MICHAEL
THOMPSON 3476  0001-01-01 00:00:00.000000
 x'06004001000000000000000000FA9023' 74904229642240 000030 SALLY
KWAN 4738  0001-01-0100:00:00.000000

(2) MANAGER1 执行如下的 UPDATE 语句,未提交确认:

1
2
3
4
UPDATE EMPLOYEE SET
  (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
  WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
  ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

(3) MANAGER2 使用隔离级别 UR 执行 SELECT 的结果输出如下:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 141285665533242120 000010 CHRISTINE
HAAS 1092  2008-12-20 16:47:03.125000
 x'05004001000000000000000000FA9023' 74904229642240 000020 MICHAEL
THOMPSON 3476  0001-01-01 00:00:00.000000
 x'06004001000000000000000000FA9023' 74904229642240 000030 SALLY
KWAN 4738  0001-01-01 00:00:00.000000

(4) MANAGER1 提交 UPDATE 语句,提交后输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'0400400100000000
 0000000000FA9023' 141285665533242120 000010 CHRISTINE
HAAS 1092  2008-12-20
 16:47:03.125000
 x'0500400100000000
 0000000000FA9023' 74904229642240 000020 MICHAEL
THOMPSON 3476  0001-01-01
 00:00:00.000000
 x'0600400100000000
 0000000000FA9023' 74904229642240 000030 SALLY
KWAN 4738  0001-01-01
 00:00:00.000000

(5) MANAGER2 执行如下的 UPDATE 语句:

1
2
3
4
UPDATE EMPLOYEE SET
  (FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1090')
  WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
  ROW CHANGE TOKEN FOR EMPLOYEE=141285665533242120

(6) MANAGER1 提交修改而 MANAGER2 尝试更新相同的数据,结果如下:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO  FIRSTNME  LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 141285667099502664  000010  CHRISTINE 
HAAS  1090  2008-12-20 16:51:53.125000
 x'05004001000000000000000000FA9023' 74904229642240  000020  MICHAEL
THOMPSON  3476  0001-01-01 00:00:00.000000
 x'06004001000000000000000000FA9023' 74904229642240  000030  SALLY 
KWAN  4738  0001-01-01 00:00:00.000000

MANAGER2 最后执行的更新成功,因为应用程序从 MANAGER1 处读取的是未提交的更新; MANAGER2 的UPDATE语句中的ROW CHANGE TOKEN谓词成功,因为 MANAGER1 使用新标记提交了修改。
(7) MANAGER1 发出 ROLLBACK 而不是 COMMIT 时的输出结果如下:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 74904229642240 000010 CHRISTINE
HAAS 3978  0001-01-01 00:00:00.000000
 x'05004001000000000000000000FA9023' 74904229642240 000020 MICHAEL
THOMPSON 3476  0001-01-01 00:00:00.000000
 x'06004001000000000000000000FA9023' 74904229642240 000030 SALLY
KWAN 4738  0001-01-01 00:00:00.000000

MANAGER1 对修改执行回滚后,MANAGER2 试图根据 MANAGER1 的未提交的UPDATE执行数据更新, MANAGER2 最后执行的更新失败,因为当 MANAGER1 回滚到初始标记时,ROW CHANGE TOKEN谓词失败,因此UPDATE无法找到特定行。
场景 4
EMPLOYEE 表不包含 ROW CHANGE TIMESTAMP 列,并且 MANAGER1 和 MANAGER2 同时访问该表。 MANAGER1 选择一行并尝试更新它。然而,从他进行选择到更新期间,MANAGER2 更新了同一数据页中的其他数据 ( 不一定与 MANAGER1 处理的数据相同,但是位于另一行中 ) 。因此,当 MANAGER1 尝试更新数据时,更新将会失败。 这个场景的主要步骤如下:
(1) MANAGER2 执行如下 UPDATE 语句:
UPDATE EMPLOYEE SET

1
2
3
(FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
  WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
  ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

(2) MANAGER2 执行 UPDATE,成功后的输出结果如下:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO
 x'04004001000000000000000000FA9023' 141285645885181032 000010 CHRISTINE
HAAS  1092
 x'05004001000000000000000000FA9023' 141285645885181032 000020 MICHAEL
THOMPSON  3476
 x'06004001000000000000000000FA9023' 141285645885181032 000030 SALLY
KWAN  4738

(3) MANAGER1 试图对同一数据页中的另一行执行更新,UPDATE 语句如下所示:

1
2
3
4
UPDATE EMPLOYEE SET
  (FIRSTNME,LASTNAME,PHONENO) = ('MICHAEL','THOMPSON','9012')
  WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
  ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

MANAGER1 执行更新失败,因为在比较标记时,由于所有行的 ROW CHANGE TOKEN 值发生了变化 ( 即使 MANAGER1 尝试进行更新的行实际上也没有发生变化 ),ROW CHANGE TOKEN谓词失败。如果向 EMPLOYEE 表添加行修改时间戳列,则误判场景中的UPDATE不会发生失败。
场景 5
EMPLOYEE 表创建时不包含 ROW CHANGE TIMESTAMP 列,但建表后添加了一个 ROW CHANGE TIMESTAMP 列。 MANAGER1 和 MANAGER2 访问该表。 MANAGER1 从中选择一行并尝试更新。然而,从他进行选择到更新期间,MANAGER2 更新了同一数据页中的其他数据 ( 不一定与 MANAGER1 处理的数据相同,而是位于另一行中 ) 。由于已经添加了 ROW CHANGE TIMESTAMP 列,所以如果对不同行进行更新,即使位于相同页面,更新也会成功。这个场景的主要步骤如下:
(1) MANAGER2 执行如下的 UPDATE 语句:
UPDATE EMPLOYEE SET

1
2
3
(FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
  WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
  ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

(2) MANAGER2 执行 UPDATE,成功后的输出结果如下:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 141285673714388072 000010 CHRISTINE
HAAS 10922008-12-20 18:22:25.593000
x'05004001000000000000000000FA9023' 74904229642240 000020 MICHAEL
THOMPSON 3476  0001-01-01 00:00:00.000000
 x'06004001000000000000000000FA9023' 74904229642240 000030 SALLY
KWAN 4738  0001-01-01 00:00:00.000000

(3) MANAGER1 对同一数据页上的另一行执行下面的 UPDATE 语句:

1
2
3
4
UPDATE EMPLOYEE SET
 (FIRSTNME,LASTNAME,PHONENO) = ('MICHAEL','THOMPSON','9012')
 WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
 ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

(4) MANAGER1 执行 UPDATE 可以成功,成功后的输出结果如下:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 141285673714388072 000010 CHRISTINE
HAAS 1092  2008-12-20 18:22:25.593000
 x'05004001000000000000000000FA9023' 141285673726689984 000020 MICHAEL
THOMPSON 9012  2008-12-20 18:22:37.312000
 x'06004001000000000000000000FA9023' 74904229642240 000030 SALLY
KWAN 4738  0001-01-01 00:00:00.000000

场景 6
EMPLOYEE 表具有一个 ROW CHANGE TIMESTAMP 列,并且只有 MANAGER1 访问该表。 MANAGER1 从中选择若干行并尝试更新它们。然而,从他进行选择到更新期间,表被离线重组。稍后,当 MANAGER1 尝试更新数据时,更新失败。行更新失败是因为执行REORG后 ROW CHANGE TIMESTAMP 列发生了变化。这个场景的主要步骤如下:
(1) 执行下面的命令重组 EMPLOYEE 表:

1
REORG TABLE EMPLOYEE

(2) 重组表 EMPLOYEE 后,EMPLOYEE 表的 ROW CHANGE TIMESTAMP 列发生变化,输出结果如下所示:

1
2
3
4
5
6
7
RID_BIT ROW CHANGE TOKEN EMPNO FIRSTNME LASTNAME  PHONENO ROW CHANGE TIMESTAMP
 x'04004001000000000000000000FA9023' 141285781563232400 000010 CHRISTINE
HAAS 3978  2008-12-21 11:29:30.250000
 x'05004001000000000000000000FA9023' 141285781563232401 000020 MICHAEL
THOMPSON 3476  2008-12-21 11:29:30.250001
 x'06004001000000000000000000FA9023' 141285781563232402 000030 SALLY
KWAN 4738  2008-12-21 11:29:30.250002

(3) MANAGER1 在运行REORG后尝试更新数据,SQL 语句如下:
UPDATE EMPLOYEE SET

1
2
3
(FIRSTNME,LASTNAME,PHONENO) = ('CHRISTINE','HAAS','1092')
  WHERE RID_BIT(EMPLOYEE)=x'04004001000000000000000000FA9023' AND
  ROW CHANGE TOKEN FOR EMPLOYEE=74904229642240

MANAGER1 执行更新失败,因为在 MANAGER1 执行SELECT 和 UPDATE期间,另一个任务对表执行了离线重组,因此在将执行SELECT时检索到的标记和当前标记进行比较时,ROW CHANGE TOKEN谓词失败。因此,UPDATE语句无法找到具有ROW CHANGE TOKEN的行,这些行是在发生REORG之前检索得到的。
乐观锁总结
为了避免在使用悲观锁定技术时可能引发的锁等待问题,乐观锁定技术最小化了给定资源对于其他事务的不可用时间。由于数据库管理器可以确定行发生修改的时间,它可以确保数据完整性,同时限制持有锁的时间。通过使用乐观并发控制,数据库管理器在完成读操作之后可以立即释放行或页锁。
DB2 V9.5 支持进行更简便和快捷的乐观锁定,而且避免了误判的发生。这些支持通过行标识符 (RID_BITRID) 内置函数、ROW CHANGE TOKEN表达式、基于时间的更新检测和隐式隐藏列实现。使用这种编程模型的应用程序可以从增强的乐观锁定特性要受益,并且能够进一步增强并发性。其实 DB2 V9.5 的乐观锁机制在某种程度上类似 Oracle 数据库的 SCN 实现机制,相信 DB2 在以后的版本中会对这种技术做进一步的改进,从而实现对某行操作的多版本读。

6.4  内部锁

我们在数据库中经常会看到一些内部锁,最常见的是内部 P 锁和内部 V 锁,下面来介绍这些内部锁。

6.4.1  内部方案锁 (Internal Plan Lock)

首先,我们先做一个例子,如果输出结果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C:\>db2 +c update employee set salary=1234 where empno='000100'
 DB20000I  SQL 命令成功完成。 -- 注:执行一条新的SQL语句,+C不提交,否则锁消失了
 C:\>db2 get snapshot for locks on sample----- 监控加锁情况
  数据库锁定快照
数据库名称  = SAMPLE
数据库路径  = C:\DB2_01\NODE0000\SQL00001\
输入数据库别名  = SAMPLE
挂起的锁定   = 3
当前已连接的应用程序   = 2
……………………………………………… . 节省篇幅 , 略…………………………………………………………………………
锁定名称  = 0x53514C4332473133B7F3CE3241
锁定属性   = 0x00000000
发行版标志   = 0x40000000
锁定计数   = 1
挂起计数   = 0
锁定对象名   = 0
对象类型=内部方案锁定(Internal Plan Lock)方式   = S
……………………………………………… . 节省篇幅 , 略…………………………………………………………………………

大家知道无论数据库是在处理静态 SQL 还是动态 SQL,DB2 的相关组件都需要去访问程序包信息和相应的 SQL 查询访问计划。那么默认情况下,DB2 都会去给相应的 Package Cache 加内部 P 锁,以防止其他的什么操作将正在使用的 Package 删除,毫无疑问的是,这样的一个锁对性能多少会有影响,但影响是很小的。
DB2 的变量注册参数里面有个配置参数是 DB2_APM_PERFORMANCE 。如果这个值被设置为 ON,则会启用无包锁定方式。此方式允许全局查询高速缓存运行,而不必使用包锁定,这些锁定是内部系统锁定,可以保护高速缓存的包条目不被除去。无包锁定方式在一定程度上可以提高性能,但它不允许执行某些数据库操作。这些被禁止的操作可能包括:使包无效的操作、使包不起作用的操作、PRECOMPILE、BIND 和 REBIND 。所以,在这里只是让大家知道这个变量和了解程序包高速缓存的内部工作机制,但是不建议大家修改这个值。
在上面的例子中,我们可以看到有一个内部 P 锁。这是因为无论我们执行的是一条静态 SQL 语句还是一条动态 SQL 语句,在执行过程中,都要依次经过语法检查、语义检查、权限检查、查询重写、下推分析等阶段,最后优化器根据统计信息、配置参数、索引等情况为这条 SQL 语句生成一个成本最优的执行方案。该方案决定了使用什么扫描方式、访问表的顺序、表的连接方式和使用哪个索引。那么为了保证这条 SQL 语句在下次执行时不再重复上述步骤,数据库会把这条 SQL 语句的执行方案存放在程序包高速缓存中 (PCKCACHE_SZ) 中,在存放的时候它首先需要在程序包高速缓存中申请一块内存,然后把执行方案 INSERT 到该内存中。在申请时它要在这块内存中加一个锁以确保在 SQL 语句执行期间,这块内存不被别的应用删除 (DROP),这就是内部 P 锁的作用 ( 在 Oracle 数据库中这种锁叫 LATCH 。 LATCH 是对内存加的锁,不是对数据库对象 ( 表、行 ) 加的锁,所以这种锁我们可以忽略 ) 。

6.4.2  内部 V 锁 (Internal Variation Lock)

当执行动态 SQL 语句时,对 SQL 的处理会在程序包的缓存中存储一个变量条目,为了保证这个变量条目在事务处理期间的有效性,DB2 会为它赋予一个内部 V 锁。下面我们看一个例子,在一个 B/S 结构中,前台运行 Java 应用,应用通过连接池发起动态 SQL 语句到数据库。假设某个时刻应用执行如下 SQL 语句:

1
update account set transdate= date(current timestamp) -? day

在这条 SQL 语句执行期间,我们执行“ get snapshot for locks ”监控发现如下锁的信息,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<a href="http://www.dbforums.com/archive/index.php/t-479205.html"><code>Application</code></a> handle = 40
 Application ID = 0A0B0A49.EB44.020821194232
 Sequence number = 0001
 Application name = db2jcc
 Authorization ID = DB2INST1Application status = Compiling --注:应用程序正在编译
 Status change time = 08-21-2008 9:52:10.800363
 Application code page = 819
 Locks held = 2
 Total wait time (ms) = 0
 List Of  Locks
 Lock Object Name = 0
 Node number lock is held at = 0Object Type = Internal V Lock --注:内部V锁
 Tablespace Name =
 Table Schema =
 Table Name =
 Mode = S
 Status = Granted
 Lock Escalation = NO
 Lock Object Name = 0
 Node number lock is held at = 0Object Type = Internal P Lock --注:内部P锁
 Tablespace Name =
 Table Schema =
 Table Name =
 Mode = S
 Status = Granted
 Lock Escalation = NO
 --------------------------------- 略 -----------------------------------

通过上面的锁的信息的监控,我们发现存在内部 V 锁。下面我们来讲解内部 V 锁的加锁机制。
在“ update account set transdate=date(current timestamp) -? day ”这条 SQL 语句传递给优化器编译 (COMPILING) 时,我们需要为这条 SQL 语句提供一个编译环境,这个编译环境包括把特殊的寄存器 (CURRENT STAMP) 变量转变成真正的值;表 ACCOUNT 没有模式名,需要使用 DEFAULT QUALIFIER 用作模式;我们使用了 DATE 函数,需要知道该函数的函数路径 (CURRENT FUNCTION PATH) ;我们使用了参数标记 ( ? ),需要把参数标记转变成真正的值。上述的每一个变量都需要一个内部 V 锁来保证在该 SQL 执行期间,这些变量不被别的 DDL 语句修改或者被程序包缓存空间管理算法删除。

6.4.3  内部 S 锁

DB2 中的很多动作实际是作为一个事件来记录的,那么事件的序号的机制保证这些事件是按照一定的正确的顺序来运转。也就是一个特定的代理 AGENT 只能一次处理一个特定的事件。
在分区数据库环境中,这样的内部 S 锁通常是用来在执行无效的 DDL 语句的时候,初始化特定的临时表空间。
在 DB2 V7 到现在的版本中,这样的内部 S 锁实际是作用于临时表空间上的 S 锁。一个特定应用程序都会有自己的一个存在于特定临时表空间里面的临时表,这样的内部 S 锁作用其上,以控制其不被其他的操作删除了相应的临时表空间。内部 S 锁通常用在分区数据库环境中。

6.4.4  内部 C 锁

内部 C 锁 (Internal Catalog Cache Lock) 是为编目缓存 (CATALOGCACHE_SZ) 中某个条目而得到的一个锁,这样的锁用来在编目缓存中参照或者修改任意的条目的保证其完整性和一致性。 DB2 的编目缓存用来缓存系统编目表的信息,以加速 SQL 语句的编译速度和缩短 SQL 语句的编译过程。
从 DB2 组件设计的角度来看,当编目缓存的条目信息被锁的时候,相应的系统编目表的数据行也必须被锁。
所以我们的内部 C 锁的格式和常规的表的数据行的锁的格式是一样的,如下所示:
表空间id+数据库对象id+数据行id
只不过在这样的锁的名字中,会有个 'C' 用来表示这个锁是 Catalog Cache 锁。“ R ”用来表示这个锁是数据记录的锁。在一个事务的处理过程中,如果系统编目表的信息被更新,那么相应的编目缓存的条目的锁就是 X 锁,通常情况下,只会在相应的条目上加 S 锁。
DB2 内部保证在 SQL 处理的过程中只需要对编目缓存持有 S 锁。当 SQL 编译结束,相应的执行段被创建时,DB2 的这个相应的编目缓存条目的锁就会被释放。下面我们举一个内部 C 锁的例子,输出结果如下:

1
2
C:\>db2 +c create table t2(id int)
 DB20000I  SQL 命令成功完成。

在另外一个窗口中使用“ get snapshot for locks ”监控信息的输出结果,如下所示 ( 部分 ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
--------------------------------- 省略 -----------------------------------
锁定名称  =0x0000050004431A00600E407D43  --
注:Tablespaceid+Tableid+RID锁定属性  = 0x00000000 
发行版标志  = 0x40000000
锁定计数 = 255
挂起计数  = 0
锁定对象名  = 0
对象类型  = 内部目录高速缓存锁定
方式  = X
 
锁定名称  = 0x0100000000000000600E407D43 --
注:43代表Catalog Cache Lock锁定属性 = 0x00000000  --
注:52 代表是行锁 (row lock)
发行版标志  = 0x40000000
锁定计数  = 255
挂起计数  = 0
锁定对象名  = 0
对象类型  = 内部目录高速缓存锁定
方式  = X
 --------------------------------- 省略 -----------------------------------
锁定名称  = 0x0300000000000000000000004F
锁定属性  = 0x00000000
发行版标志  = 0x40000000
锁定计数  = 2
挂起计数  = 0
锁定对象名  = 0
对象类型  = 内部对象表锁定
方式  = IN
锁定名称  = 0x03001600000000000000000054
锁定属性  = 0x00000000
发行版标志  = 0x40000000
锁定计数 = 255
挂起计数  = 0
锁定对象名  = 22
对象类型  = 表
表空间名  = IBMDB2SAMPLEREL
方式  = Z
 --------------------------------- 省略 -----------------------------------

6.4.5  其他内部锁

除了上述我们常见的一些内部锁外,DB2 数据库还存在以下几种内部锁:
Inplace Reorg Lock
我们会经常对表做 REORG ,REOGR 操作会将陈旧的 RID 进行更新。负责控制并发的扫描器会持有这个锁,直到该扫描器发现没有别的应用请求这个旧的 RID 。然后扫描器就释放这个锁,这时我们就可以对这个行做在线 REORG 了。当我们的表很大的时候,如果你不厌烦阅读数据库的快照监控信息的话,你会发现 REORG 总是在等待 Inplace Reorg Lock 。同时在在线重组期间,数据库还会产生一个内部改变锁定,来对重组的表进行碎片整理。下面我们举一个 Inplace Reorg Lock 的例子,输出结果如下:

1
2
3
C:\>db2 +c reorg table DB2ADMIN.dept1 inplace allow read access
 DB20000I  REORG 命令成功完成。
 DB21024I  此命令为异步的,可能未能立即生效。

在另外一个窗口中使用而“ get snapshot for locks ”监控信息输出,结果如下 ( 部分 ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
锁定列表
锁定名称   = 0x0300050002000000000000006A
锁定属性   = 0x00000000
发行版标志   = 0x40000000
锁定计数   = 1
挂起计数   = 1
锁定对象名   = 2
对象类型=原地重组锁定(inplace reorg lock)表空间名   = IBMDB2SAMPLEREL
表模式   = DB2ADMIN
表名   = DEPT1
方式   = IN
锁定名称  = 0x03000500000000000000000074 -- 注:0300 代表表空间 id ; 0500 表示表 ID
锁定属性   = 0x00000000
发行版标志   = 0x40000000
锁定计数   = 1
挂起计数   = 1
锁定对象名   = 5
对象类型=内部表改变锁定
表空间名   = IBMDB2SAMPLEREL
表模式   = DB2ADMIN
表名   = DEPT1
方式   = X
 
锁定名称   = 0x03000500000000000000000054
锁定属性   = 0x00000000
发行版标志   = 0x40000000
锁定计数   = 1
挂起计数   = 1
锁定对象名   = 5
对象类型   = 表
表空间名   = IBMDB2SAMPLEREL
表模式   = DB2ADMIN
表名   = DEPT1
方式   = S

内部 L 锁
Internal Long/LOBLock(L-lock) 负责对大对象进行处理。
内部联机备份锁 OLB-LOCK
当我们执行数据库在线备份时,会在表空间级别获得 OLB-LOCK 锁。它主要处理在线备份期间我们对数据库的更改。
内部 O-Lock
Internal-Object Table 主要负责提交 (COMMIT) 同步。
自动调整大小锁定
在 DB2 V9 中很多数据库配置参数默认都被设置为 AUTOMATIC,当我们执行某个特定操作时,DB2 会执行自动调整大小锁定。例如,当我们执行备份时,DB2 会自动调整 UTIL_HEAP_SZ 内存大小。
自动存储器锁定
在 DB2 V8.2.2 以后,如果我们建数据库时使用自动存储 (AUTOMATIC STORAGE),那么在我们做某些操作时,数据库就会有自动存储锁定。
下面我们举一个例子,这个例子中包含了内部联机备份锁、自动调整大小锁和自动存储器锁三种内部锁。输出结果如下:

1
C:\>db2 backup db sample online- 注:sample 数据库为自动存储,DB2 版本是 V9.5

在另外一个窗口中使用“ get snapshot for locks ”监控信息输出,结果如下 ( 部分 ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
应用程序句柄  = 1125
应用程序标识  = *LOCAL.DB2_01.081220102511
序号  = 00001
应用程序名  = db2bp.exe
 CONNECT 授权标识  = DB2ADMIN
应用程序状态   = 正在执行备份
状态更改时间  = 2008-12-20 18:24:55.783743
应用程序代码页  = 1208
挂起的锁定  = 18
总计等待时间 ( 毫秒 )  = 0
锁定列表
锁定名称  =0x0400000000000000000000006F- 注:表空间ID锁定属性  = 0x00000000
发行版标志  = 0x40000000
锁定计数  = 1
挂起计数  = 0
锁定对象名  = 4
对象类型  = 内部联机备份锁定
表空间名  = SYSTOOLSPACE
方式  = X
锁定名称  = 0x0700000000000000000000005A
锁定属性  = 0x00000000
发行版标志  = 0x40000000
锁定计数  = 1
挂起计数  = 0
锁定对象名  = 7
对象类型  = 自动调整大小锁定
方式  = S
锁定名称  = 0x0000000000000000000000007A
锁定属性 = 0x00000000
发行版标志  = 0x40000000
锁定计数  = 1
挂起计数  = 0
锁定对象名  = 0
对象类型  = 自动存储器锁定
方式  = S
锁定名称  = 0x00000000000000000000000070
锁定属性  = 0x00000000
发行版标志  = 0x40000000
锁定计数  = 1
挂起计数  = 0
锁定对象名  = 0
对象类型  = 表空间
表空间名  = SYSCATSPACE
方式 = IN

上述我们给大家讲解了 DB2 的一些内部锁,其实这些内部锁是 DB2 内部的一种自我保护机制,它类似 Oracle 数据库的 LATCH( 但也不完全一样,因为 Oracle 的 LATCH 只对内存加锁,而 DB2 的内部锁有些对内存加锁,有些对数据库对象加锁 ) 。这些内部锁在通常情况下我们可以不用关注,在这里之所以给大家讲解这么多,主要想要大家了解 DB2 数据库的内部结构。更重要的是,真相知道得越多,越有信心,知其然并且知其所以然。

6.5  设置锁相关的注册变量

DB2 从版本 8 以后先后引入了三个 DB2 注册变量—— DB2_EVALUNCOMMITTED、DB2_SKIPDELETED 和 DB2_SKIPINSERTED 来提高并发。
为什么要引入这三个变量呢?在 DB2 没有这三个变量前,如果一个用户正在更改 (UPDATE)、插入 (INSERT) 或删除 (DELETE) 一行,那么 DB2 会在这一行加上排它锁 (EXCLUSIVE),别的用户不能读写,除非使用 UR 隔离级别。其实目前市场上除了 Oracle 外,Informix、SQL Server 和 Sybase 等数据库对锁的控制采用的都是这种方式。而 Oracle 数据库有回滚段 (ROLLBACK SEGMENT),在 Oracle 数据库中对于 INSERT 操作,回滚段记录插入记录的 ROWID ;对于 UPDATE 操作,回滚段记录更新字段的旧值 (BEFORE IMAGE) ;对于 DELETE 操作,回滚段记录整行的数据。由于 Oracle 有了回滚段,可以实现多版本读,所以在用 Oracle 进行数据库开发时,很少关注锁的情况,因为大部分情况下应用都是可以读的,只不过有的时候大不了读以前的“ BEFORE IMAGE ”罢了。所以很多使用 Oracle 进行开发的用户在转向 DB2 开发时,都特别郁闷。而 DB2 为了改善应用程序并发性,从 DB2 V8 以后就陆续引入了这三个变量。这三个变量也是 DB2 客户提出要求 IBM 改进的,这种需求最初是由 SAP 提出的。这三个变量并不会改变锁的本质,只不过是了解它们的工作方式和机制可以使我们根据我们的业务逻辑来合理地设置调整以提高应用程序并发。
下面我们先通过一个例子来说明没有这三个变量之前的一些锁的情况。假设 T1 表中有 5 条记录,分别为 11、22、33、44、55 。其中第 2 条记录 22 被删除了,现在有一个 Session 1 要重新插入一条新的记录 22 ;同时第二个 Session 2 执行了“ db2 select * from t1 where id >11 and id<44 ”,正常的话它应该检索到 33 这条记录,但是由于现在插入的记录 22 也包含在这个谓词限定范围内,所以这个时候 Session 2 处于 LOCKWAIT 状态,输出结果如下:

1
2
3
Session 1   Session 2
 db2 CONNECT TO SAMPLE db2 +c INSERT INTO t1 VALUES(22) 
 db2 CONNECT TO SAMPLE db2 SELECT * FROM t1 WHERE id >11 and id <44

我们通过监控看到的输出结果如图 6-6 所示。
图 6-6  逻辑输出结果
从 DB2 的角度来说好像这是合理的,但是从用户角度和业务逻辑来说希望这个时候能够读取到数据,那么怎么解决这个矛盾呢?下面我们来仔细讲解这三个变量。

6.5.1  DB2_EVALUNCOMMITTED

DB2 V8.1.4 版本中首次引入了 DB2_EVALUNCOMMITTED 这个 DB2 注册表变量。当它被启用 (=TRUE | ON | YES | 1) 时,它将修改 DB2 中只读查询的行为,以减少锁冲突,使之允许在索引扫描 ( 必须是 type-2 索引,对于 type-1 索引该特性不受支持 ) 或表访问时推迟锁,直到限定语句的所有谓词都是已知的。引入这个新的注册表变量是为了可选地提高一些应用程序的并发性,其实质是允许读扫描推迟或避免行锁,只能获得那些符合某个谓词的行上的锁,而并不是获得被检查的所有行上的锁。直到适合特定查询的一个数据记录成为已知。
注意:
在 DB2 V8.1 和更高版本中,所有新索引都创建为 type-2 索引。一个例外是当您在已具有 type-1 索引的表上添加索引时,仅在这种情况下,新索引也将是 type-1 索引。要了解一个表存在什么类型的索引,执行 INSPECT CHECK 命令。要将 type-1 索引转换为 type-2 索引,执行 REORG INDEXES CONVERT 命令。
在 DB2 V8.1.4 之前 ( 并且没有设置这个注册表变量 ),DB2 将执行保守式的锁:在验证行是否满足查询的排除谓词之前,它将锁定每个被访问的行。不管数据行是否被提交,以及根据语句的谓词它是否被排除,对于索引扫描和表访问都执行这样的锁定操作。下面我们举一个简单的例子,输出结果如下:

1
2
3
db2 create table t1(id int)
 db2 insert into t1 values(11)
 db2 commit

现在有两个 Session 分别发出了下面的 SQL 语句,输出结果如下:

1
2
3
Session 1   Session 2
 db2 CONNECT TO SAMPLE db2 +c INSERT INTO t1 VALUES(22)
 db2 CONNECT TO SAMPLE db2 SELECT * FROM t1

我们查看 Session 2 的状态,输出结果如图 6-7 所示。
图 6-7  查看 Session 的状态
第一条语句“ db2 +cinsert into table t1 values(22) ”阻塞所有其他 Session 的扫描,因为它持有行上的锁。如果第二个 Session 执行“ db2 select * form t1 ”那么它将被阻塞,直到事务 1 提交或回滚。但是我们假设第二条语句是“ db2 select * form t1 where id=11 ”。在此情况下,即使 Session 2 与列 ID=22 中的任何值 ( 还没有被提交 ) 都没有关系,它也仍将被阻塞,处于锁等待 (LOCK WAIT) 状态。在 DB2 中,默认情况下将发生这一系列的事件,因为默认的隔离级别是 CS 。这种隔离级别表明,一个查询访问的任何一行在游标定位到该行时都必须被锁定。在语句 1 释放它用于更新表 T1 的锁之前,语句 2 不能包含表 T1 第一行上的锁。如果 DB2 知道 ID=11 值不是语句 2 的数据请求的一部分 ( 换句话说,它在锁行之前计算了谓词 ),那么就可以避免阻塞,这是合情合理的,因为语句 2 将不会尝试锁定表中的第一行。
现在我们启用 DB2_EVALUNCOMMITTED 注册变量,该实例设置后需要重启实例才能生效,输出结果如下:

1
2
3
db2set DB2_EVALUNCOMMITTED=ON – i
 db2stop force
 db2start

在启用该实例后,再重复刚才的实验,我们发现第二条 SQL 语句“ select * from t1 where id=11 ”可以执行而不会被阻塞。所以 DB2_EVALUNCOMMITTED 注册变量的作用是判断该 SQL 谓词所扫描的行是否有锁,如果没有就可以检索到数据。
EVAL UATE UNCOMMITTED 第一次在 DB2 V8.1.4 中被引入时,带有以下限制:
该特性只能用于 CS 和 RS 隔离级别。
SARGABLE 谓词必须存在,以便计算。
不适用于系统编目表上的扫描。
当扫描一个 MDC 表时,对于索引扫描,块锁可以推迟。然而,对于表扫描,块锁不会推迟。
被推迟的锁不会发生在正在执行在线表重组的表上。
INDEX MANAGER 不可能在没有锁行的情况下回调 DATA MANAGER 来取数据记录。这意味着 ISCAN-FETCH 计划不能在 DATA MANAGER 中推迟锁 ( 唯一的例外是对一个 MDC 表的块索引,它的 INDEX EVALUATION 谓词是一个 ISCAN 计划 ) 。
DB2 V8.2.2 通过去掉 DB2 V8.1.4 中第一阶段的 EVAL UATE UNCOMMITTED,改进了这些缺点。 DB2 V8.2.2 引入了名为 DEFERISCANFETCH 的注册表变量,作为 DB2_EVALUNCOMMITTED 的新设置。启用该变量时,由该特性承担的锁将避免使用 ISCAN-FETCH 数据读取。
DB2_EVALUNCOMMITTED 注册变量影响 DB2 在游标稳定性 (CS) 和读稳定性 (RS) 隔离级别下的行锁机制。当你启用该功能时,DB2 可以对未提交的插入 (INSERT) 或者更新 (UPDATE) 数据进行谓词判断,如果未提交数据不符合这条语句的谓词判断条件,DB2 将不对未提交数据加锁。这样就避免了因为要对未提交数据加锁而引起的锁等待状态,提高了应用程序访问的并发性,同时 DB2 在无条件进行表扫描时会忽略删除的行数据 ( 不管是否提交 ) 。
这里分两种情况来看待。第一种情况:对于插入 (INSERT) 或者更新 (UPDATE),如果未提交数据不符合这条语句的谓词判断条件,DB2 将不对未提交数据加锁。这样虽然比不上 Oracle 对于符合这条语句的谓词判断条件可以从回滚段里面读出 BEFORE IMAGE 那样做到写不阻止读,但是起码一定程度上缓解了锁的问题,不会因为插入 (INSERT) 或者更新 (UPDATE) 一条记录造成整个表都锁住。这点是个进步,我个人觉得也不会造成什么大的负面影响。
下面我们通过一个实验来说明这点,输出结果如下:

1
2
3
4
db2 create table t1(id int)
 db2 insert into t1 values(11)
 db2 insert into t1 values(22)
 db2 commit 现在表中有两条记录 11 和 22

现在有两个 Session 发出了下面的 SQL 语句,输出结果如下:

1
2
3
Session 1   Session 2
 db2 CONNECT TO SAMPLE  db2 +c delete from t1 where id=22
 db2 CONNECT TO SAMPLE  db2 SELECT * FROM t1 WHERE id=11

在未设置 DB2_EVALUNCOMMITTED=ON 时,Session 2 肯定是处于锁等待 (LOCKWAIT) 状态的,现在我们设置了 DB2_EVALUNCOMMITTED=ON 后,我们来看看 Session 2 能否检索到数据,输出结果如下:

1
2
3
4
5
6
7
Session 1   Session 2
 db2 CONNECT TO SAMPLE  db2 +c delete from t1 where id=22
 db2 CONNECT TO SAMPLE  C:\>db2 select * from t1
                 ID
                 -----------
 11
                  1 条记录已选择。

第二种情况是:通过上面的实验我们发现在启用 DB2_EVALUNCOMMITTED=ON 时,对于 DELETE 操作,DB2 在无条件进行表扫描时会忽略删除的行数据 ( 不管是否提交 ) 。个人觉得有很大的问题,通过上面的这个测试,一个会话删除一条记录并没有提交,另外一个会话查询的时候已经没有这条记录了,这相当于 UR 隔离级别。这样显然是不符合业务要求的。与其这样还不如锁住。所以启用 DB2_EVALUNCOMMITTED=ON 时,对于删除操作应该注意多多测试。
现在我们在 T1 上创建一个 type-2 的索引,然后再来做刚才的那个实验:

1
db2 create index index11 on t1(id)

在两个命令行窗口中分别发出下面的 SQL 语句:

1
2
3
4
Session 1    Session 2
 db2 CONNECT TO SAMPLE   id=22  db2 CONNECT TO SAMPLE
 db2 create index index11 on t1(id)  db2 +c delete from t1 where
 C:\>db2 select * from t1   --lockwait 挂起

我们在另外一个窗口查看 Session 2 的状态,发现 Session 2 处于 LOCKWAIT 状态,输出结果如图 6-8 所示。
图 6-8  查看 Session 2 的状态
当您的 DB2 环境中启用了 EVAL UATE UNCOMMITTED 行为时,您应该清楚,谓词计算可能发生在未提交的数据上。而且,在表扫描访问中,被删除行会被无条件忽略,而对于 type-2 索引扫描,被删除的键不会被忽略 ( 除非您还设置了 DB2_SKIPDELETED 注册表变量,DB2_SKIPDELETED 变量我们稍后介绍 ) 。如果您要在 DB2 环境中单独设置 DB2_SKIPDELETED 注册表变量,DB2 将允许在表扫描访问时无条件地忽略被删除行,并忽略 type-2 索引扫描访问的伪删除索引键。

6.5.2  DB2_SKIPDELETED

DB2_SKIPDELETED 变量被启用时,将允许使用 CS 或 RS 隔离级别的语句在索引扫描期间无条件地跳过被删除的键,而在表访问期间则无条件地跳过被删除的行。当DB2_EVALUNCOMMITTED被启用时,被删除的行会被自动跳过,但是除非同时启用了DB2_SKIPDELETED,否则 type-2 索引中未提交的伪删除键不会被跳过。
在上面的实验中,我们发现当我们设置了DB2_EVALUNCOMMITTED 变量时,如果表上有 type-2 索引,那么在我们读取数据时,被删除的索引键不会被忽略。这种情况下如果你希望跳过被删除的键,可以通过设置 DB2_SKIPDELETED=ON 来实现,下面我们做个实验,输出结果如下

1
2
3
db2set DB2_SKIPDELETED=ON – i
 db2stop force
 db2start

设置生效后,我们接着做刚才的实验,输出结果如下:

1
2
3
4
5
6
7
Session 1    Session 2
 db2 CONNECT TO    SAMPLE db2 CONNECT TO SAMPLE
 db2 create index index11 on t1(id)   C:\>db2 select * from t1
 ---DB2 V8 后创建的索引默认都是 type-2  ID
 db2 +c delete from t1 where id=22  -----------
    11
                         1 条记录已选择。

我们可以看到在设置 DB2_SKIPDELETED=ON 后,即使 T1 表上有 type-2 索引,扫描的时候也仍然忽略这个删除的行。但是这个在用的时候一定要结合业务逻辑使用,因为这种情况下等同于“脏读”,所以一定多测试。

6.5.3  DB2_SKIPINSERTED

虽然当一个行由于一个未提交的 INSERT 而被锁的时候,这种行为是正确的,但是有些情况下应用程序的所有者希望 DB2 忽略正在等待提交的被插入的行,就好像它不存在一样 ( 由于未提交 INSERT 的提交版本现在根本没有行,所以这是可能的 ) 。例如,银行在下午 5 点左右想统计今天的业务量,这时只是想了解大概的业务量而不是精确的,这种情况下如果启用该变量,那么遗漏一两笔业务是可以接受的。
在 DB2 V8.2.2 中,DB2_SKIPINSERTED=OFF 是默认设置。这使得 DB2 的行为和预期的一样:扫描器一直等到 INSERT 事务提交或回滚,然后返回数据。是否打开该变量取决于您的应用程序以及和业务逻辑相关的数据完整性的特征,这样可能合适,也可能不合适。例如,考虑一个涉及两个应用程序的业务流程,这两个应用程序使用相同的一个表来交换业务信息,例如一个信用评级应用程序和一个信用评分引擎。应用程序 A 基于一个 Web 表单将数据插入数据库,应用程序 B 读这些数据。为了加快信用审批的速度,由于候选者通过信用评级应用程序来进行表单转移,信息块通过表单中的“ STEPS ”被发送到应用程序 B( 通过公共的表 ) 。当候选者完成信用评级应用程序流程中的每个步骤时,信息被发送。在这个环境中,数据必须由第二个应用程序按照表中给出的顺序来处理,以便当接下来要读的行要被应用程序 A 插入时,应用程序 B 必须等待,直到 INSERT 被提交。
如果设置 DB2_SKIPINSERTED=ON,DB2 将把未提交的 INSERT( 只适于 CS 和 RS 隔离级别 ) 看作它们还没有被插入。该特性增加了并发性,同时又不牺牲隔离语义。 DB2 为扫描器实现了这种能力,通过锁属性和锁请求的反馈,使其忽略未提交的插入行,而不是等待。
下面我们来看看设置 DB2_SKIPINSERTED 变量前后的对比,输出结果如下:

1
2
3
4
5
6
Session 1    Session 2
 db2 CONNECT TO SAMPLE    db2 CONNECT TO SAMPLE
 db2 create index index11 on t1(id)  C:\>db2 select * from t1
 db2 reorg indexes all for table   -- 挂起,处于 lockwait 状态
 DB2ADMIN.t1 convert
 db2 +c insert into t1 values(33)

通过监控发现 Session 2 处于 LOCKWAIT 状态,输出结果如图 6-9 所示。
图 6-9  查看 Session 2 的状态
如果这种情况下 Session 2 希望能够跳过未提交 INSERT 的数据而得到数据,那么可以设置 DB2_SKIPINSERTED 注册变量,输出结果如下:

1
2
3
db2set DB2_SKIPINSERTED=ON – i
 db2stop force
 db2start

在设置 DB2_SKIPINSERTED=ON 后,再重复刚才的实验,我们发现这个时候,Session 2 可以读到数据,输出结果如下:

1
2
3
4
5
6
7
Session 1    Session 2
 db2 CONNECT TO SAMPLE   C:\>db2 select * from t1
 db2 create index index11 on t1(id)   ID
 db2 reorg indexes all for table   -----------
 DB2ADMIN.t1 convert   11
 db2 +c insert into t1 values(33)  22
      2 条记录已选择。

DB2_EVALUNCOMMITTED、DB2_SKIPDELETED 和 DB2_SKIPINSERTED 总结
总的来说这 3 个注册变量会影响到并发性。通过合理设置这些变量可以改善并发性,但是也会影响到应用程序的行为。建议在 DB2 开发设计的初期启用这些注册变量,从而在实现并发性增强后执行全面测试中的所有单元测试。
通过对这三个注册变量进行设置,可以提高并发,但是我们使用的时候一定要结合自己的业务逻辑使用。根据个人的经验,我建议使用 DB2_EVALUNCOMMITTED=ON 和 DB2_SKIPINSERTED=ON,对于 DB2_SKIPDELETED 变量,使用的时候一定要充分地测试,因为它等同于使用 UR 隔离级别 ( 注:虽然 DB2_SKIPINSERTED=ON 也等同于 UR,但是没有插入的数据反正也没有插入,读不到数据在业务上是可以接受的 ) 。
总之,有了这三个变量,我们多了一份选择总比没有强。目前这三个变量都是实例级别的变量,如果能做成 SQL 级或应用级的就更好了。期待 DB2 能够在后续的版本中继续对锁并发作出改进。

6.6  本章小结

数据库中锁的技术是数据库系统中的一个核心技术,它决定了数据库中访问数据的最基本的方式和手段。所以对于 DBA 来说,掌握数据库的锁技术是掌握数据库技术时必不可少的内容。
本章我们介绍了 DB2 中关于锁的一些高级内容。锁对数据库的并发影响很大,掌握锁的这些相关的技术对于我们合理地设计数据库和应用程序以保证提高数据库的并发性能具有重大意义。锁同样对数据的一致性也有很大的影响,使用不同的隔离级别策略来保证应用程序在访问 DB2 中数据的过程中所必须的数据一致性需求也显得很重要。虽然这两者都很重要,但遗憾的是数据库中数据的并发性和一致性始终是一对矛盾,如何调和这两者的关系在应用设计和数据库设计中就显得极为重要。首先考虑的因素是应用的需求,然后要考虑的就是数据库能够提供的技术手段。 DB2 的锁技术现在越来越灵活、越来越强大,通过使用本章介绍的这些高级技术或者新的锁功能,基本上都能够实现我们各种各样的数据访问中的锁需求。那么相对来说的更灵活且不易确定的就是应用中对锁的需求到底是怎样的?这就需要系统设计人员能够洞悉应用的需求,从需求定义中释出合理的锁需求,这同样也是我们要掌握的技术。本章还详细介绍了 DB2 提供的实现上述两种需求的各种高级技术和新功能,而且通过大量的例子为我们演示了这些技术的使用方法和技巧。这都为更好地掌握 DB2 的锁技术铺平了道路。

相关主题

 developerWorks Information Management 专区,了解关于信息管理的更多信息。查找技术文档、操作文章、培训、下载、产品信息等信息。
阅读本书的 第 2 章:DB2 进程体系结构
阅读本书的 第 6 章:高级锁
阅读本书的 第 9 章:高级诊断
更多推荐书籍,请访问 developerWorks 图书频道
通过 通过改善锁提高应用程序并发性,了解 DB2 锁等待、死锁和锁升级等有关的锁相关的问题。
通过 分析 DB2 for Linux, UNIX, and Windows 中的锁等待情形,如何使用 DB2 for Linux, UNIX, and Windows 的 db2pd 和 db2pdcfg 实用程序解决所等待问题。
通过 使用 DB2 V9.5 乐观锁定特性改善并发性,了解增强的乐观锁定特性,并了解使用这种编程模型的应用程序如何从中获益并进一步改善并发性。
通过 DB2 9.5 中多线程架构的工作原理,了解当您需要经常性地监视进程或线程、需要理解数据库正在使用多少内存,或者当您需要简化备份、恢复和前滚等任务关键型工作时,DB2 9.5 中的新功能如何提供帮助。
现在可以免费使用 DB2 。下载 DB2 Express-C,这是为社区提供的 DB2 Express Edition 的免费版本,它提供了与 DB2 Express Edition 相同的核心数据特性,为构建和部署应用程序奠定了坚实的基础。
下载 信息管理软件试用版,体验它们强大的功能。

转载请标明出处: 
http://blog.csdn.net/forezp/article/details/69696915 
本文出自方志朋的博客

一、spring cloud简介

spring cloud 为开发人员提供了快速构建分布式系统的一些工具,包括配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等。它运行环境简单,可以在开发人员的电脑上跑。另外说明spring cloud是基于springboot的,所以需要开发中对springboot有一定的了解,如果不了解的话可以看这篇文章:2小时学会springboot。另外对于“微服务架构” 不了解的话,可以通过搜索引擎搜索“微服务架构”了解下。

二、创建服务注册中心

在这里,我们需要用的的组件上Spring Cloud Netflix的Eureka ,eureka是一个服务注册和发现模块。
2.1 首先创建一个maven主工程。
2.2 然后创建2个model工程:一个model工程作为服务注册中心,即Eureka Server,另一个作为Eureka Client。
下面以创建server为例子,详细说明创建过程:
右键工程->创建model-> 选择spring initialir 如下图:
下一步->选择cloud discovery->eureka server ,然后一直下一步就行了。
创建完后的工程的pom.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.forezp</groupId>
<artifactId>eurekaserver</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>eurekaserver</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!--eureka server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>

<!-- spring boot test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>


</project>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
2.3 启动一个服务注册中心,只需要一个注解@EnableEurekaServer,这个注解需要在springboot工程的启动application类上加:
@EnableEurekaServer@SpringBootApplicationpublic class EurekaserverApplication {

public static void main(String[] args) {
SpringApplication.run(EurekaserverApplication.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
**2.4 **eureka是一个高可用的组件,它没有后端缓存,每一个实例注册之后需要向注册中心发送心跳(因此可以在内存中完成),在默认情况下erureka server也是一个eureka client ,必须要指定一个 server。eureka server的配置文件appication.yml:
server:
port: 8761

eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
1
2
3
4
5
6
7
8
9
10
11
12
通过eureka.client.registerWithEureka:false和fetchRegistry:false来表明自己是一个eureka server.
2.5 eureka server 是有界面的,启动工程,打开浏览器访问: 
http://localhost:8761 ,界面如下:
No application available 没有服务被发现 ……^_^ 
因为没有注册服务当然不可能有服务被发现了。

三、创建一个服务提供者 (eureka client)

当client向server注册时,它会提供一些元数据,例如主机和端口,URL,主页等。Eureka server 从每个client实例接收心跳消息。 如果心跳超时,则通常将该实例从注册server中删除。
创建过程同server类似,创建完pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.forezp</groupId>
<artifactId>service-hi</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>service-hi</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>


</project>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
通过注解@EnableEurekaClient 表明自己是一个eurekaclient.
@SpringBootApplication@EnableEurekaClient@RestControllerpublic class ServiceHiApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceHiApplication.class, args);
}

@Value("${server.port}")
String port;
@RequestMapping("/hi")
public String home(@RequestParam String name) {
return "hi "+name+",i am from port:" +port;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
仅仅@EnableEurekaClient是不够的,还需要在配置文件中注明自己的服务注册中心的地址,application.yml配置文件如下:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/server:
port: 8762
spring:
application:
name: service-hi

1
2
3
4
5
6
7
8
9
10
11
需要指明spring.application.name,这个很重要,这在以后的服务与服务之间相互调用一般都是根据这个name 。 
启动工程,打开http://localhost:8761 ,即eureka server 的网址:
你会发现一个服务已经注册在服务中了,服务名为SERVICE-HI ,端口为7862
这时打开 http://localhost:8762/hi?name=forezp ,你会在浏览器上看到 :
hi forezp,i am from port:8762
源码下载:https://github.com/forezp/SpringCloudLearning/tree/master/chapter1

四、参考资料

springcloud eureka server 官方文档
springcloud eureka client 官方文档

优秀文章推荐:



转载请标明出处: 
http://blog.csdn.net/forezp/article/details/69788938 
本文出自方志朋的博客
在上一篇文章,讲了服务的注册和发现。在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯是基于http restful的。Spring cloud有两种服务调用方式,一种是ribbon+restTemplate,另一种是feign。在这一篇文章首先讲解下基于ribbon+rest。

一、ribbon简介

Ribbon is a client side load balancer which gives you a lot of control over the behaviour of HTTP and TCP clients. Feign already uses Ribbon, so if you are using @FeignClient then this section also applies.
—–摘自官网
ribbon是一个负载均衡客户端,可以很好的控制htt和tcp的一些行为。Feign默认集成了ribbon。
ribbon 已经默认实现了这些配置bean:

二、准备工作

这一篇文章基于上一篇文章的工程,启动eureka-server 工程;启动service-hi工程,它的端口为8762;将service-hi的配置文件的端口改为8763,并启动,这时你会发现:service-hi在eureka-server注册了2个实例,这就相当于一个小的集群。访问localhost:8761如图所示:

三、建一个服务消费者

重新新建一个spring-boot工程,取名为:service-ribbon; 
在它的pom.xml文件分别引入起步依赖spring-cloud-starter-eureka、spring-cloud-starter-ribbon、spring-boot-starter-web,代码如下:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.forezp</groupId>
<artifactId>service-ribbon</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>service-ribbon</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>


</project>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
在工程的配置文件指定服务的注册中心地址为http://localhost:8761/eureka/,程序名称为 service-ribbon,程序端口为8764。配置文件application.yml如下:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/server:
port: 8764
spring:
application:
name: service-ribbon
1
2
3
4
5
6
7
8
9
在工程的启动类中,通过@EnableDiscoveryClient向服务中心注册;并且向程序的ioc注入一个bean: restTemplate;并通过@LoadBalanced注解表明这个restRemplate开启负载均衡的功能。
@SpringBootApplication@EnableDiscoveryClientpublic class ServiceRibbonApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceRibbonApplication.class, args);
}

@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
写一个测试类HelloService,通过之前注入ioc容器的restTemplate来消费service-hi服务的“/hi”接口,在这里我们直接用的程序名替代了具体的url地址,在ribbon中它会根据服务名来选择具体的服务实例,根据服务实例在请求的时候会用具体的url替换掉服务名,代码如下:
@Servicepublic class HelloService {

@Autowired
RestTemplate restTemplate;

public String hiService(String name) {
return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
写一个controller,在controller中用调用HelloService 的方法,代码如下:
/**
* Created by fangzhipeng on 2017/4/6.
*/@RestControllerpublic class HelloControler {

@Autowired
HelloService helloService;
@RequestMapping(value = "/hi")
public String hi(@RequestParam String name){
return helloService.hiService(name);
}


}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在浏览器上多次访问http://localhost:8764/hi?name=forezp,浏览器交替显示:
hi forezp,i am from port:8762
hi forezp,i am from port:8763
这说明当我们通过调用restTemplate.getForObject(“http://SERVICE-HI/hi?name=“+name,String.class)方法时,已经做了负载均衡,访问了不同的端口的服务实例。

四、此时的架构

源码下载:https://github.com/forezp/SpringCloudLearning/tree/master/chapter2

五、参考资料

本文参考了以下:
spring-cloud-ribbon
springcloud ribbon with eureka
服务消费者

转载请标明出处: 
http://blog.csdn.net/forezp/article/details/69808079 
本文出自方志朋的博客
上一篇文章,讲述了如何通过RestTemplate+Ribbon去消费服务,这篇文章主要讲述如何通过Feign去消费服务。

一、Feign简介

Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性,可使用Feign 注解和JAX-RS注解。Feign支持可插拔的编码器和解码器。Feign默认集成了Ribbon,并和Eureka结合,默认实现了负载均衡的效果。
简而言之:

二、准备工作

继续用上一节的工程, 启动eureka-server,端口为8761; 启动service-hi 两次,端口分别为8762 、8773.

三、创建一个feign的服务

新建一个spring-boot工程,取名为serice-feign,在它的pom文件引入Feign的起步依赖spring-cloud-starter-feign、Eureka的起步依赖spring-cloud-starter-eureka、Web的起步依赖spring-boot-starter-web,代码如下:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.forezp</groupId>
<artifactId>service-feign</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>service-feign</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>


</project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
在工程的配置文件application.yml文件,指定程序名为service-feign,端口号为8765,服务注册地址为http://localhost:8761/eureka/ ,代码如下:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/server:
port: 8765
spring:
application:
name: service-feign
1
2
3
4
5
6
7
8
9
10
在程序的启动类ServiceFeignApplication ,加上@EnableFeignClients注解开启Feign的功能:
@SpringBootApplication@EnableDiscoveryClient@EnableFeignClientspublic class ServiceFeignApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceFeignApplication.class, args);
}
}

1
2
3
4
5
6
7
8
9
10
11
定义一个feign接口,通过@ FeignClient(“服务名”),来指定调用哪个服务。比如在代码中调用了service-hi服务的“/hi”接口,代码如下:
/**
* Created by fangzhipeng on 2017/4/6.
*/@FeignClient(value = "service-hi")
public interface SchedualServiceHi {
@RequestMapping(value = "/hi",method = RequestMethod.GET)
String sayHiFromClientOne(@RequestParam(value = "name") String name);
}

1
2
3
4
5
6
7
8
9
10
11
在Web层的controller层,对外暴露一个”/hi”的API接口,通过上面定义的Feign客户端SchedualServiceHi 来消费服务。代码如下:
@RestControllerpublic class HiController {

@Autowired
SchedualServiceHi schedualServiceHi;
@RequestMapping(value = "/hi",method = RequestMethod.GET)
public String sayHi(@RequestParam String name){
return schedualServiceHi.sayHiFromClientOne(name);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
启动程序,多次访问http://localhost:8765/hi?name=forezp,浏览器交替显示:
hi forezp,i am from port:8762
hi forezp,i am from port:8763
Feign源码解析:http://blog.csdn.net/forezp/article/details/73480304
本文源码下载: 
https://github.com/forezp/SpringCloudLearning/tree/master/chapter3

五、参考资料

spring-cloud-feign

优秀文章推荐:



转载请标明出处: 
http://blog.csdn.net/forezp/article/details/69934399 
本文出自方志朋的博客
在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),在Spring Cloud可以用RestTemplate+Ribbon和Feign来调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。
为了解决这个问题,业界提出了断路器模型。

一、断路器简介

Netflix has created a library called Hystrix that implements the circuit breaker pattern. In a microservice architecture it is common to have multiple layers of service calls.
. —-摘自官网
Netflix开源了Hystrix组件,实现了断路器模式,SpringCloud对这一组件进行了整合。 在微服务架构中,一个请求需要调用多个服务是非常常见的,如下图:
较底层的服务如果出现故障,会导致连锁故障。当对特定的服务的调用的不可用达到一个阀值(Hystric 是5秒20次) 断路器将会被打开。
断路打开后,可用避免连锁故障,fallback方法可以直接返回一个固定值。

二、准备工作

这篇文章基于上一篇文章的工程,首先启动上一篇文章的工程,启动eureka-server 工程;启动service-hi工程,它的端口为8762。

三、在ribbon使用断路器

改造serice-ribbon 工程的代码,首先在pox.xml文件中加入spring-cloud-starter-hystrix的起步依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId></dependency>
1
2
3
4
在程序的启动类ServiceRibbonApplication 加@EnableHystrix注解开启Hystrix:
@SpringBootApplication@EnableDiscoveryClient@EnableHystrixpublic class ServiceRibbonApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceRibbonApplication.class, args);
}

@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
改造HelloService类,在hiService方法上加上@HystrixCommand注解。该注解对该方法创建了熔断器的功能,并指定了fallbackMethod熔断方法,熔断方法直接返回了一个字符串,字符串为”hi,”+name+”,sorry,error!”,代码如下:
@Servicepublic class HelloService {

@Autowired
RestTemplate restTemplate;

@HystrixCommand(fallbackMethod = "hiError")
public String hiService(String name) {
return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}

public String hiError(String name) {
return "hi,"+name+",sorry,error!";
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
启动:service-ribbon 工程,当我们访问http://localhost:8764/hi?name=forezp,浏览器显示:
hi forezp,i am from port:8762
此时关闭 service-hi 工程,当我们再访问http://localhost:8764/hi?name=forezp,浏览器会显示:
hi ,forezp,orry,error!
这就说明当 service-hi 工程不可用的时候,service-ribbon调用 service-hi的API接口时,会执行快速失败,直接返回一组字符串,而不是等待响应超时,这很好的控制了容器的线程阻塞。

四、Feign中使用断路器

Feign是自带断路器的,在D版本的Spring Cloud中,它没有默认打开。需要在配置文件中配置打开它,在配置文件加以下代码:
feign.hystrix.enabled=true
基于service-feign工程进行改造,只需要在FeignClient的SchedualServiceHi接口的注解中加上fallback的指定类就行了:
@FeignClient(value = "service-hi",fallback = SchedualServiceHiHystric.class)
public interface SchedualServiceHi {
@RequestMapping(value = "/hi",method = RequestMethod.GET)
String sayHiFromClientOne(@RequestParam(value = "name") String name);
}
1
2
3
4
5
6
SchedualServiceHiHystric需要实现SchedualServiceHi 接口,并注入到Ioc容器中,代码如下:
@Componentpublic class SchedualServiceHiHystric implements SchedualServiceHi {
@Override
public String sayHiFromClientOne(String name) {
return "sorry "+name;
}
}
1
2
3
4
5
6
7
8
启动四servcie-feign工程,浏览器打开http://localhost:8765/hi?name=forezp,注意此时service-hi工程没有启动,网页显示:
sorry forezp
打开service-hi工程,再次访问,浏览器显示:
>
hi forezp,i am from port:8762
这证明断路器起到作用了。
五、Hystrix Dashboard (断路器:Hystrix 仪表盘)
基于service-ribbon 改造,Feign的改造和这一样。
首选在pom.xml引入spring-cloud-starter-hystrix-dashboard的起步依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
在主程序启动类中加入@EnableHystrixDashboard注解,开启hystrixDashboard:
@SpringBootApplication@EnableDiscoveryClient@EnableHystrix@EnableHystrixDashboardpublic class ServiceRibbonApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceRibbonApplication.class, args);
}

@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
打开浏览器:访问http://localhost:8764/hystrix,界面如下:
点击monitor stream,进入下一个界面,访问:http://localhost:8764/hi?name=forezp
此时会出现监控界面:
本文源码下载: 
https://github.com/forezp/SpringCloudLearning/tree/master/chapter4

六、参考资料

circuit_breaker_hystrix
feign-hystrix
hystrix_dashboard

优秀文章推荐:



什么是java
   java不仅仅是一门编程语言,还是一个由一系列计算机软件和规范形成的技术体系。
    java技术体系(狭义):
           java程序设计语言
          各种硬件平台上的java虚拟机
          class文件格式
          java api类库
         来自商业机构和开源社区的第三方java类库
   java技术体系(广义):
           java、scala、groovy等一系列运行在jvm虚拟机上的语言、jvm虚拟机、开发API、相关框架和工具都属于java技术体系
   
什么是jdk?
         java程序设计语言、java虚拟机、java api类库这三部分统称为JDK(Java Development Kit)
         jdk是用于支持java程序开发的最小环境,注意:这儿是说java程序开发,不是java程序运行
        
什么是jre?
        jre即java运行环境,包括jvm虚拟机和java api类库中的java se,这是java程序运行的标准(最小)环境,这儿就可以在使用上和jdk 区分了,因为我们的java程序都依赖着java标准开发api(java se),所以jre必须包括java se.
    
什么是java虚拟机?
       java虚拟机是java语言的运行程序,基于具体的操作系统和硬件平台,是java语言下面的运行环境的抽象和实现,包括class文件的解释执行、即时编译、内存分配、垃圾回收、多线程、并发控制。
       java虚拟机使得java程序可以一次编写,到处运行。
       实际上java虚拟机就是一个运行在操作系统上的java.exe程序,这个程序负责运行我们的java程序(class文件)。
       注意:
       1.javac、javap等工具并不属于虚拟机范畴,它们基本都是使用基于jvm虚拟机运行的java程序,javac负责编译java源文件到class文件,javap负责反编译class文件,所以这些工具应该属于jdk范畴。 
       2.java虚拟机实际上只包括那个java.exe、配置文件、动态库,但运行一个java程序光有java虚拟机是不行的,因为一个java程序里面还依赖着object等相关类,这些类属于java se。java se的本身就是一大堆java类和他们的本地方法实现,这些包括在jre目录下面的rt.jar等文件中。

 jdk和jre的关系
       jdk包含jre, jdk还包括java语言本身、编译等小工具、除开java se的java api类库。
      看看本笔记最后几张图就能体会了-----
 
看图理解java技术体系
     
java技术体系的四个平台
         
java的多种虚拟机实现
    1.classic虚拟机:第一款商用java虚拟机,纯解释执行,想编译执行就必须外挂jit编译器,但相关的jit编译器会接管虚拟机的执行系统,从而生成没有经过高度优化的本地指令,解释器也无法工作了。
    2.Exact VM:具备现代虚拟机的雏形,含有准确式内存管理(虚拟机可以知道内存中某个位置),解释+两级即时编译等,寿命不长
    3.Hotspot虚拟机:现代使用最广泛的虚拟机,包含一系列优秀特性,java界三大虚拟机之一。
    4.JRockit虚拟机:纯编译执行的虚拟机,特别适用于服务器应用,号称最快的虚拟机,java界三大虚拟机之一。
    5.J9虚拟机:东家的虚拟机,windows平台不稳定,但和websephere服务器、AIX系统结合很棒,也号称最快的虚拟机,java界三大虚拟机之一,前几年才开源,叫做openj9
    6.Azul VM/BEA liquid VM:sun的实验性虚拟机
    7.Apache Harmony/Google Android Dalvik VM:前者是IBM为了与sun争夺java霸权,而主导研发的一个开源虚拟机,已经不了了之。后者是google公司是为了andorid平台而基于前者研发的虚拟机,坦白说已经不叫java虚拟机,因为没有遵守jvm规范,只是能够运行java程序罢了,含有自己的字节码格式,运行基于寄存器指令架构,确实很优秀!
    8.微软jvm:微软为了争夺java在windows平台领导权而研发,比当时的sun的虚拟机快,因为sun的控诉而死亡。
java的未来
    9.IKVM:基于.net平台的虚拟机,.net本身也是一个虚拟机,哈哈,这个虚拟机可以实现在.net平台运行java程序,也支持转换java->c#,不过项目领导人已经宣布退出,后续开发维护不太乐观。
    10.其他数十款虚拟机。

java的未来
     1.模块化:jigsaw项目在java9已经实现,实际上模块化在ibm领导的osgi联盟当中已经成熟。
     2.混合语言:groovy,scala
     3.多核并行
     4.语法糖
     5.64位虚拟机:64位虚拟机

补充--jdk目录说明
This chapter introduces the JDK directories and the files they contain. The file structure of the JRE is identical to the structure of the jre directory in the JDK.
This chapter covers the following topics:

Demos and Samples

Demos and samples that show you how to program for the Java platform are available as a separate download at the Java SE Downloads page at
These are available as separate .tar.z compressed packages and .tar.gz compressed binaries. Similar to other 64-bit bundles on Oracle Solaris, the 64-bit demos and samples bundles on Oracle Solaris require that the 32-bit demos and samples bundles to also be installed.

Development Files and Directories

This section describes the most important files and directories required to develop applications for the Java platform. Some of the directories that are not required include Java source code and C header files. See Additional Files and Directories.
jdk1.8.0
bin
java*
javac*
javap*
javah*
javadoc*
lib
tools.jar
dt.jar
jre
bin
java*
lib
applet
ext
jfxrt.jar
localdata.jar
fonts
security
sparc
server
client
rt.jar
charsets.jar
Assuming the JDK software is installed at /jdk1.8.0, here are some of the most important directories:
/jdk1.8.0
Root directory of the JDK software installation. Contains copyright, license, and README files. Also contains src.zip, the archive of source code for the Java platform.
/jdk1.8.0/bin
Executables for all the development tools contained in the JDK. The PATH environment variable should contain an entry for this directory.
/jdk1.8.0/lib
Files used by the development tools. Includes tools.jar, which contains non-core classes for support of the tools and utilities in the JDK. Also includes dt.jar, the DesignTime archive of BeanInfo files that tell interactive development environments (IDEs) how to display the Java components and how to let the developer customize them for an application.
/jdk1.8.0/jre
Root directory of the Java Runtime Environment (JRE) used by the JDK development tools. The runtime environment is an implementation of the Java platform. This is the directory referred to by the java.home system property.
/jdk1.8.0/jre/bin
Executable files for tools and libraries used by the Java platform. The executable files are identical to files in /jdk1.8.0/bin. The java launcher tool serves as an application launcher (and replaced the earlier jre command that shipped with 1.1 releases of the JDK). This directory does not need to be in the PATH environment variable.
/jdk1.8.0/jre/lib
Code libraries, property settings, and resource files used by the JRE. For example rt.jar contains the bootstrap classes, which are the run time classes that comprise the Java platform core API, and charsets.jar contains the character-conversion classes. Aside from the extsubdirectory, there are several additional resource subdirectories not described here.
/jdk1.8.0/jre/lib/ext
Default installation directory for extensions to the Java platform. This is where the JavaHelp JAR file goes when it is installed, for example. This directory includes the jfxrt.jar file, which contains the JavaFX runtime libraries and the localedata.jar file, which contains the locale data for the java.text and java.util packages. See The Extension Mechanism at
/jdk1.8.0/jre/lib/security
Contains files used for security management. These include the security policy java.policy and security properties java.security files.
/jdk1.8.0/jre/lib/sparc
Contains the .so (shared object) files used by the Oracle Solaris release of the Java platform.
/jdk1.8.0/jre/lib/sparc/client
Contains the .so file used by the Java HotSpot VM client, which is implemented with Java HotSpot VM technology. This is the default Java Virtual Machine (JVM).
/jdk1.8.0/jre/lib/sparc/server
Contains the .so file used by the Java HotSpot VM server.
/jdk1.8.0/jre/lib/applet
JAR files that contain support classes for applets can be placed in the lib/applet/ directory. This reduces startup time for large applets by allowing applet classes to be preloaded from the local file system by the applet class loader and provides the same protections as though they had been downloaded over the Internet.
/jdk1.8.0/jre/lib/fonts
Font files used by the platform.

Additional Files and Directories

This section describes the directory structure for Java source code, C header files, and other additional directories and files.
jdk1.8.0
db
include
man
src.zip
/jdk1.8.0/src.zip
Archive that contains the source code for the Java platform.
/jdk1.8.0/db
Contains Java DB. See Java DB Technical Documentation at
/jdk1.8.0/include
C-language header files that support native-code programming with the Java Native Interface and the Java Virtual Machine (JVM) Debugger Interface. See Java Native Interface at
See also Java Platform Debugger Architecture (JPDA) at
/jdk1.8.0/man
Contains man pages for the JDK tools.

补充--open-jdk目录说明
      

OpenJDK8 directory structures

See directory structure of various OpenJDK projects (download the tree command for Ubuntu or install it via sudo apt-get install tree)
Below are full or partial outputs of the output from the tree command:
OpenJDK (level 1) 
$ tree -L 1 -d 
OpenJDK (level 2) 
$ tree -L 2 -d 
build (level 3) 
$ tree -L 3 -d build
build
└── linux-x86_64-normal-server-release
├── corba ⇐ Common Object Request Broker Architecture
│ ├── btclasses
│ ├── btjars
│ ├── classes
│ ├── dist
│ ├── gensrc
│ ├── lib
│ └── logwrappers
├── hotspot ⇐ Java, the platform (one part)
│ ├── dist
│ ├── linux_amd64_compiler2
│ └── linux_amd64_docs
├── images
│ ├── j2re-image ⇐ this folder contains Java 8 JRE
│ ├── j2sdk-image ⇐ this folder contains Java 8 JDK
│ ├── lib
│ ├── local_policy_jar.tmp
│ ├── src
│ ├── _strip_jdk
│ ├── _strip_jre
│ ├── symbols
│ ├── unsigned
│ └── US_export_policy_jar.tmp
├── jaxp ⇐ Java API for XML Processing
├── jaxws ⇐ Java API for Web Services
├── jdk ⇐ Java, the language (other part)
├── langtools ⇐ supporting tools for Java, the language
├── nashorn ⇐ Javascript Runtime for the JVM
└── tmp
Folder jaxp onwards the tree structure has been collapsed to enabling fitting of the build folder structure on this page.
Hotspot (all levels) 
$ tree -d hotspot
Only a partial view of the hotspot structure has been displayed on this page to enable us to fit the overview.

补充--hostspot目录




 
                       


一.运行时数据区域

1.程序计数器
   程序计数器是一块较小的的内存空间,可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅仅是概念模型),字节码解释器工作时就是通过改变这个计数器的值来取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器。
  每个线程都有一个程序计数器,如果线程执行的是一个java方法,这个计数器记录的就是当前正在执行的虚拟机字节码指令的地址;如果执行的是native方法,这个计数器值则为空。该内存区域是唯一一个在java虚拟机规范当中没有规定任何OutOfMemoryError情况的区域。
 补充:并不是native方法就没有程序计数器,这儿所讲的程序计数器是jvm之上的程序区域,实际上操作系统线程还带有自己的程序计数器!!两个属于不同的层面。

2.虚拟机栈
  虚拟机栈也是线程私有的,生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型。
  每个方法在执行的同时都会创建一个栈帧(每个栈帧都是虚拟机栈的一个元素)用于存放局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
  局部变量表包括了基本数据类型、对象引用和返回地址,局部变量表在编译期间完成完成分配,当进入一个方法时,这个方法需要在栈中分配多大的局部变量表空间是完全确定。
  在jvm虚拟机规范中,这个区域规定了两种异常状况:
  1.如果请求的栈深度大于虚拟机所准许的深度,抛出StackOverflowError这个往往是由于调用方法的深度太深造成,无限递归!
  2.如果虚拟机栈可以动态扩展(大部分虚拟机都可以,不过虚拟机规范中也准许固定长度的虚拟机栈-xss参数),如果栈在扩展时无法申请到内存,就会抛出OutOfMemoryError.

3.本地方法栈
  本地方法栈与虚拟机栈的所用类似,只不过虚拟机栈是是为虚拟机执行java方法(字节码)服务,而本地方法栈则为虚拟机是用到的Native方法服务。
  虚拟机规范中没有对本地方法栈使用的语言、使用方式与数据结构有规定,有些虚拟机(hotspot)直接就把本地方法栈和虚拟机栈合二为一。
  本地方法栈也会抛出StackOverflowError和OutMemoryError.

4.java堆
   Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,是虚拟机所管理的内存中最大的一块。此内存区域的唯一目的就是【存放对象实例和数组】,几乎所有的对象实例和数组都在这里分配内存。
   在java虚拟机规范当中描述的是:所有的对象实例以及数组都要在堆中分配。 但是随着JIT编译器和逃逸分析技术的逐渐成熟,栈上分配、标量分配等优化技术可能让对象并不一定在堆上分配。
   Java堆是垃圾收集器管理的主要区域,也称为GC 垃圾堆,
         从内存回收的角度看,由于现在的收集器基本都采用分代收集算法,所以java堆还可以细分为新生代和老年代;新生代再细致一点还有Eden空间,From Survivor空间、To Survivor空间等区域。
         从内存分配的角度看,线程共享的堆还可能划分出多个线程私有的分配缓冲区(TLAB)。
         无论如何划分,都与存放内容无关。无论哪个区域存储的都是对象实例,进一步划分的目的是为了更好的回收内存或者更快的分配内存。
   java堆可以处于物理上的连续或者不连续的内存空间当中,只要逻辑连续。实现可以固定大小,也可以扩展。主流虚拟机都是按照可扩展来实现的(-Xmx,-Xms)
   如果在堆中没有内存完成实例分配,并且堆也无法扩展时,就会抛出OOM.
   补充:字符串常量也是对象,不过字符串常量在方法区中,所以字符串常量对象不一定在堆当中。

5.方法区
   方法区也是一块线程共享的区域,用于存储被虚拟机加载的类信息,常量(字面量和符号引用),静态变量(静态变量是指基本数据类型,对象引用,所以静态变量指向的对象一般还是在堆上)、及时编译器编译后的代码、字符串常量池(字面量的载体)等数据。
   方法区是一个概念空间,虽然java虚拟机规范把方法区描述为堆的一个逻辑部分( 搞笑的是还有一个名字“非堆”),但是事实上各个版本的虚拟机对其的实现是不同的:
          Hotspot虚拟机在java8之前是将堆中的一部分用来实现方法区,这个被称为永久代(可通过-XX:MaxPermSize来控制上限),hotspot在java7中已经把字符串常量池从永久代中移出到本地内存当中,在java8中直接取消了永久代,将方法区整个使用本地内存使用,这个被称为元空间。
          j9和JRockit根本就没有永久代的说法,方法区一直是用本地内存实现。
  基于堆中的永久代来实现方法区的实现比较简单,基本和堆中其它部分的设计差不多,垃圾回收也是直接使用已有算法,但很容易导致内存溢出。
  基于本地内存实现的方法区实现很复杂,需要单独的垃圾回收,不过不容易导致内存溢出,但是进程和操作系统的内存也是有极限的,还是可能会造成内存溢出。
  java虚拟机规范对方法区的描述中还说明方法区可以不实现垃圾收集,因为方法区的很多数据是不会被回收的 ,该区域的内存回收主要是对常量池的回收和类型的卸载,很多时候回收不到什么东西,但是这部分的垃圾回收是必要的!
  java虚拟机规范规定,当方法区无法满足内存分配需求时,将抛出OOM
        hotspot在java8之前通过设置永久代容量来控制方法区大小,java之后的元空间默认没有限制,容量极限就是进程内存极限。不过可以通过-XX:MetaspaceSize=N和-XX:MaxMetaspaceSize来控制!
6.运行时常量池
      运行时常量池属于方法区的一部分!
      Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的字面量和符号引用,这部分内容(也可以称为 .Class文件中的静态常量池)将在类加载后进入方法区的运行时常量池中存放。当常量池无法再申请到内存时也会抛出OutofMemoryError异常。
      常量池主要是编译时产生的字面量和符号运用,当他们在class文件中存储时被称为静态常量池,当class被加载,字面量和符号引用便进入运行时常量池。
      很多字面量会被拿来当成字符串用,这种运行时常量池又称为字符串常量池,字符串可以通过String.intern()方法动态的进入字符串常量池, String.intern()是一个本地方法(java7之后,intern方法不会把字符串的对象放进方法区,只会在方法区当中放一个指向对象的引用)。
      当常量池无法再申请内存时也会抛出OOM。
      补充
     1.java当中还存在一种被称为常量池技术的东西存在: 
       Byte,Short,Integer,Long,Character默认创建了[-128,127]的相应类型的缓存数据。当通过字面量或者ValueOf方法得到的上述封装类型如果满足上面的范围条件,就会使用常量池中存储的对象。这个不是JVM来做的,是API。
        看看Integer.ValueOf方法的实现:
       public static Integer valueOf(int i) {
            if (i >= IntegerCache.low && i <= IntegerCache.high)
                return IntegerCache.cache[i + (-IntegerCache.low)];
            return new Integer(i);
        }
      private static class IntegerCache {
            static final int low = -128;
            static final int high;
            static final Integer cache[];
      ....}
     但它们和运行时常量池不是一个概念。只是池化技术的一种API层面的实现,不属于JVM内存区域应该讨论的范畴。
     2.运行时常量池不只有一个,而是每个类一个,和他们的class对象一起放在方法区当中,不同类中的运行时常量池的结构都是一样的,只不过存放的东西是各个类中特有的字面量和符号引用。
     3.字符串常量池自java7开始就不再将字符串对象存放在方法区中,将对象放在堆,方法区放引用来指向堆中的对象
7.直接内存
   直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分区域也很常用,而且也可能导致OOM。
   在JDK1.4中新加入了NIO包,引入了一种基于通道与缓冲区的I/O方式。它可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用来对这块内存进行操作。这样能在很多场景中显著提高性能,因为避免了在java堆和native内存中来回复制数据。
  直接内存的分配不会受到java堆大小的限制,但是还是会受到本机内存、内存寻址、操作系统的限制,且有参数控制,所以并不是无限的。直接内存的分配都是调用Unsafe类中的allocat方法来分配的内存(底层是c的malloc方法)。
  直接内存的垃圾回收工作很复杂,目前jdk中能够直接操作直接内存的方法主要是ByteBuffer.allocateDirect方法和Unsafe.allocateMemory方法,前者可以直接使用,且是依靠后者来进行内存分配,后者不可以在java程序中使用,也不推荐使用,因为需要手动垃圾回收,不安全。
  服务器管理员在对JVM调优时往往会忘记考虑直接内存,切记。
  DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与java堆最大值(-Xmx指定)一样。
  参考:http://www.importnew.com/26334.html

8.内存溢出和内存泄漏
内存泄露 : 指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用,可用内存越来越少 
内存溢出 : 指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于老年代或永久代垃圾回收后,仍然无内存空间容纳新的Java对象的情况。 
内存泄露是内存溢出的一种诱因,不是唯一因素。

9.JVM运行时数据区和直接内存的思考
  JVM运行时数据区区可以理解为是基于JVM所分配和管理的内存,是可以被java相关参数调节的。这部分的内存并不是无中生有的,还是基于了直接内存。只能说是JVM去申请了直接内存,然后用这些内存来实现所谓的JVM运行时数据区。直接内存就是程序直接拿到的物理内存,这部分的内存和C拿到的内存一样,因为不受JVM控制,所以使用和调节要小心。(虽然JVM也提供了一些释放的方法)。

10.java程序的内存占用就是堆得内存占用吗?
 很多人错误的认为运行Java程序时使用-Xmx和-Xms参数指定的就是程序将会占用的内存,但是这实际上只是Java堆对象将会占用的内存。堆只是影响Java程序占用内存数量的一个因素。要更好的理解你的Java程序将会占用多大的内存需要先了解有哪些因素会影响到内存的占用。这些因素包括:
每个因素对内存占用的影响又会随着应用程序、运行环境和系统平台的不同而变化,那怎样计算总的内存占用量?是的,想得到一个准确的数字不是那么容易,因为你很难控制本地(Native)部分。你能控制的部分只有堆大小:-Xmx,类占用的内存:-XX:MaxPermSize,还有线程栈:-Xss控制每个线程占用的内存。所以,计算公式为:
(-Xmx) + (-XX:MaxPermSize) + 线程数 * (-Xss) + 其它内存
其它内存部分取决于本地代码占用的内存,如NIO、socket缓冲区等。它一般大约是jvm内存的5%左右。所以假设我们有下面的JVM参数和100个线程:
-Xmx1024m -XX:MaxPermSize=256m -Xss512k 
那么jvm进程至少会占用内存数量为:1024m + 256m + 100*512k + (0.05 * 1330m) = 1396.5m
我一般使用(1.5 * 堆最大值)来作为一个近似值表示一个tomcat进程会需要的最小内存,如果你有需要增加MaxPermSize到256M以上的应用这个值可以更大些。如果你使用这个来衡量你的系统将会占用多少内存要记住你需要为系统和其它运行在系统上的程序留下足够的内存,否则会导致系统使用过多的虚拟内存,这样会降低性能。
参考: http://www.oschina.net/question/100267_65544
 
二.hotspot虚拟机对象探秘

对象的创建、布局和访问在jvm虚拟机规范当中并没有定义,基于实用优先的原则,这儿讨论HotSpot虚拟机和它的内存区域堆中的对象。
1.对象的创建
   这儿以new指令来讨论对象的创建过程:
    (1)虚拟机遇到一条new指令,首先去检查这个指令的参数是否能在常量池中定位一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有加载,那必须先执行相应的类的类加载过程。
    (2)在类加载检查通过后,接下来虚拟机为新生对象分配内存。对象所需的内存大小在类加载完成后便可以确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
             存在两种分配情况:
             [1]如果java堆中的内存是绝对规整的(一块连续的内存空间,可以认为是数组),所有用过的内存放一边,没用过的内存放一边,用一个指针指向空闲内存的起始地址,内存分配就只需要把指针向空闲空间那边移动一段与对象大小相等的距离,这种被称为“指针碰撞”。
            [2]如果java堆中的内存并不规整,已使用的内存和空闲内存交错,虚拟机就必须维护一个队列,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象,并更新列表上的记录,这种被称为“空闲列表”。
            选择哪种分配方式由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定,因此在使用Serial、ParNew等Compact过程的收集器时(实际上可以认为是使用复制算法和标记压缩算法的收集器),系统采用的分配算法是指针碰撞,而使用CMS这种基于标记清理算法的收集器时通常采用空闲列表。因为堆中的老年代和新生代可以使用不同的收集器,因此java堆可以同时存在不同的对象内存分配方式。
            除了如何划分空间,还有一个问题就是对象创建在虚拟机中是非常频繁的行为,并发性很高,因此会遇到很多的线程问题。比如在只用指针碰撞时,当一个对象正在 分配内存,但是还没有来得及修改空闲内存指针,另外一个对象也在使用该指针分配内存就会出现问题。
           解决这种问题有三种方案:
                  一种是对分配内存空间的动作进行同步处理
                  采用CAS+失败重试(实际上是采用的这种方式)
                 最后一种是把内存分配的动作划分在不同的空间之中进行,每个线程都预先在java堆中分配一小块内存,称为本地线程分配缓存(TLAB)。哪个线程需要分配缓存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁定。虚拟机是否采用TLAB,可以通过-XX:+/-UseTLAB参数来设定。这儿实际上是一种用提前分配的内存策略,TLAB并不是线程私有的,只是在分配的时候是私有的,访问还是线程共享的。
    (3)内存分配完成后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一步也可以提前到TLAB分配时完成,这一步操作保证了对象的实例字段可以不赋初始值就可以使用,程序能访问到这些字段的数据类型所对应的零值(默认值)
    (4)接下来,虚拟机对对象进行必要的设置,比如这个类是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄。这些信息存放在对象头(Object Header)当中,根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象会有不同的设置方式。
    (5)上面的工作完成后,从虚拟机的角度看,一个新的对象已经产生,但从java程序的角度看,对象创建才刚刚开始——<init>方法还没有执行,所有的字段都还为零。所以一般来说执行完new指令后会接着执行<init>方法,把对象按照程序员的意图初始化。init方法包括给字段赋初始值、代码块、构造方法。顺序是前两者先。

   补充:
       1.作者对克隆和反序列化的理解中使用到new指令并不是指调用构造方法,这两种情况不会经过构造方法。

2.对象的内存布局
   对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
   对象头
   对象头又包括两部分信息:
          第一部分是存储对象自身的运行时数据,如哈希码、GC分代年龄、锁标志状态、线程持有的锁、偏向线程ID、偏向时间戳。这部分数据在32位和64位的虚拟机中(未开启压缩指针)中分别为32位和64位,官方称之为Mark Word。对象需要存储的运行时数据很多,实际上已经超出了记录限度,但是对象头信息是与对象自身数据无关的,虚拟机为了提升效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间存储更多的信息,它会根据对象状态复用自己的存储空间。见下图:
        
         这儿的标志位就是指锁标志位,在各种锁定的情况下,Mark Word是不同的。
       对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上报留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身(句柄)。
       如果一个对象是数组,那么对象头中还有一块用于记录数组长度的指针(4个字节,所以java数组指针的大小只能用int指定)。

   实例数据
            

   对齐填充
      
         对象填充并不是必然的存在,也没有特别的含义,仅仅只是占位符,满足虚拟机内存管理系统对对象起始地址的标准化要求。

   

3.对象的访问定位
   java程序需要使用Reference来操作堆上的对象。由于reference类型在java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用通过何种方式去定位和访问堆中的内容,目前主流的方式主要是使用句柄和直接指针两种
       如果使用句柄访问,那么java堆中将会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,而对象句柄中包括了对象的实例数据和类型数据的各自的地址信息。
             
       如果使用直接指针访问,那么对象头中就必须放置指向类型的指针(看上面)
           
       两种访问方式各有优势:
       



三.OutOfMemoryError异常
1.java堆溢出
   调节和防止堆内存溢出的关键参数和手段是:
   -Xms20M:-Xms参数调节堆的初始大小,这儿设置20M
  -Xmx20M:-Xmx参数调节堆的最大大小,这儿设置20M
  -XX:+HeapDumpOnOutOfMemoryError:该参数为当发生内存溢出时,对堆内存区域dump出内存快照,以便日后分析
  Eclipse Memory Analyzer:可以对上面对堆内存的快照进行分析
 实际使用时:首先使用分析工具检查堆内存快照,根据引用关系等检查是否有内存泄漏,如果无内存泄漏,就需要根据本机内存,适当修改堆大小,如果还是有溢出现象,需要修改程序代码,优化对象的使用时间等

2.虚拟机栈和本地方法栈溢出
   控制虚拟机栈大小的参数是-Xss,控制本地方法栈大小的参数-Xoss
   由于hotspot的虚拟机栈和本地方法栈是一体的,因此-Xoss参数没有作用。
   在java虚拟机规范中描述了两种异常情况:
   1.如果线程请求的栈深度大于虚拟机所准许的最大深度,将抛出StackOverflowError
   2.如果虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError.
  我认为虚拟机规范这样描述的目的是将递归造成的栈过于深的异常归为StackOverflowError,将栈申请内存无法满足的异常归为OOM。但是这两个存在很大的重叠,因为无法分配栈空间到底是剩余的内存太小,还是已申请的栈太深?本质上只是同一件事情的两种描述。
  在单线程环境下,以无限递归来让栈溢出的两种情况:
  1.使用-Xss参数减少栈的内存容量。结果:抛出SOE,异常出现时输出的堆栈深度随着-Xss参数的减少而减小。
  2.定义了大量的本地变量,增大此方法栈中本地变量表的长度,结果:抛出SOE,异常时输出的堆栈深度随着本地变量的增大而减小。
  实验结果表明:单线程环境下,无论是栈帧过大,还是虚拟机栈容量过小,当内存无法分配时都是抛出SOE。
  几个数据:在我的i7,8G内存机器上,无限递归时,不设置-Xss的栈最大深度是3W-4W,设置-Xss128k时为100左右
  通过多线程,不断建立新线程并且运行可以产生栈上的OOM,一般来说操作系统分配给每个进程的内存是有限制的,比如32位的windows限制为2GB。虚拟机提供了参数来控制java堆、方法区(java8不行)和直接内存的最大值,计数器的消耗可以忽略,假设  忽略虚拟机本身耗费的内存,那进程内存上限-堆最大值-方法区最大值-直接内存最大值就近似等于虚拟机栈和本地方法栈的值,所以还是有限的。每个线程分配的栈容量越大,可以建立的线程数自然就少,建立线程时就越容易耗尽内存。
  一般情况下使用虚拟机的默认参数,栈深度达到两三千是没问题的,如果是线程过多而导致内存溢出,可以减少线程数、更换64位虚拟机(这个可能同时需要增加物理内存大小),减少最大堆容量和减少栈大小来换取更多的线程。

3.方法区和运行时常量池溢出
   运行时常量池属于方法区。 
   java7之前,只要一直往字符串常量池噻东西就能导致PermGen Space的OOM。
   java7之后,往字符串常量池塞东西很难导致OOM。
   java8之前可以使用-XX:PermSize -XX:MaxPermSize来限制永久代的大小,但是java8因为永久代已经不存在,方法区就在直接内存当中,而且没有提供参数来调节这个大小(可以估计)  
   java7之后想要方法区溢出只能不停加载类。
   方法区溢出是一种常见的内存溢出现象,一个类要被垃圾收集器收集,判定条件很苛刻。在经常动态生成大量class的应用中,需要注意类的回收状况。这类常见除了CGLIB外,还有充斥大量jsp或者动态产生jsp文件的应用(jsp会被编译为class,servlet),基于OSGI的应用。



4.本机直接内存溢出
    DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与java堆最大值(-Xmx指定)一样。
    可以使用Unsafe.allocateMemory(Size)来分配直接内存
    直接内存在溢出时,不会进入dump
    如果程序发生了溢出,dump出的文件又偏小,程序又使用了NIO,那很可能问题就出现在直接内存上。










原文地址:http://www.oschina.net/question/100267_65544
很多人错误的认为运行Java程序时使用-Xmx和-Xms参数指定的就是程序将会占用的内存,但是这实际上只是Java堆对象将会占用的内存。堆只是影响Java程序占用内存数量的一个因素。要更好的理解你的Java程序将会占用多大的内存需要先了解有哪些因素会影响到内存的占用。这些因素包括:

每个因素对内存占用的影响又会随着应用程序、运行环境和系统平台的不同而变化,那怎样计算总的内存占用量?是的,想得到一个准确的数字不是那么容易,因为你很难控制本地(Native)部分。你能控制的部分只有堆大小:-Xmx,类占用的内存:-XX:MaxPermSize,还有线程栈:-Xss控制每个线程占用的内存。所以,计算公式为:
(-Xmx) + (-XX:MaxPermSize) + 线程数 * (-Xss) + 其它内存

其它内存部分取决于本地代码占用的内存,如NIO、socket缓冲区、JNI等。它一般大约是jvm内存的5%左右。所以假设我们有下面的JVM参数和100个线程:
-Xmx1024m -XX:MaxPermSize=256m -Xss512k 
那么jvm进程至少会占用内存数量为:1024m + 256m + 100*512k + (0.05 * 1330m) = 1396.5m

我一般使用(1.5 * 堆最大值)来作为一个近似值表示一个tomcat进程会需要的最小内存,如果你有需要增加MaxPermSize到256M以上的应用这个值可以更大些。如果你使用这个来衡量你的系统将会占用多少内存要记住你需要为系统和其它运行在系统上的程序留下足够的内存,否则会导致系统使用过多的虚拟内存,这样会降低性能。




  1. 堆大小设置
    JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
    典型设置:
    • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
      -Xmx3550m:设置JVM最大可用内存为3550M。
      -Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
      -Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
      -Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
    • java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
      -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
      -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
      -XX:MaxPermSize=16m:设置持久代大小为16m。
      -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
  2. 回收器选择
    JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前
    系统配置进行判断。
    1. 吞吐量优先的并行收集器
      如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
      典型配置
      • java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
        -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
        -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
        -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100
        -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
        -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
    2. 响应时间优先的并发收集器
      如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
      典型配置
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
        -XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
        -XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
      • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
        -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
        -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
  3. 辅助信息
    JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
    • -XX:+PrintGC
      输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]
                      [Full GC 121376K->10414K(130112K), 0.0650971 secs]
    • -XX:+PrintGCDetails
      输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
                      [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
    • -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
      输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
    • -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
      输出形式:Application time: 0.5291524 seconds
    • -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
      输出形式:Total time for which application threads were stopped: 0.0468229 seconds
    • -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
      输出形式:
      34.702: [GC {Heap before gc invocations=7:
       def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)
      eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)
      from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)
        to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)
       tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
      the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
       compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
         the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
          ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
          rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
      34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:
       def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
      eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
        from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
        to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)
       tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
      the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
       compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)
         the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)
          ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)
          rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)
      }
      , 0.0757599 secs]
    • -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
  4. 常见配置汇总
    1. 堆设置
      • -Xms:初始堆大小
      • -Xmx:最大堆大小
      • -XX:NewSize=n:设置年轻代大小
      • -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
      • -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
      • -XX:MaxPermSize=n:设置持久代大小
    2. 收集器设置
      • -XX:+UseSerialGC:设置串行收集器
      • -XX:+UseParallelGC:设置并行收集器
      • -XX:+UseParalledlOldGC:设置并行年老代收集器
      • -XX:+UseConcMarkSweepGC:设置并发收集器
    3. 垃圾回收统计信息
      • -XX:+PrintGC
      • -XX:+PrintGCDetails
      • -XX:+PrintGCTimeStamps
      • -Xloggc:filename
    4. 并行收集器设置
      • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
      • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
      • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
    5. 并发收集器设置
      • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
      • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

四、调优总结
  1. 年轻代大小选择
    • 响应时间优先的应用尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
    • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
  2. 年老代大小选择
    • 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
      • 并发垃圾收集信息
      • 持久代并发收集次数
      • 传统GC信息
      • 花在年轻代和年老代回收上的时间比例
      减少年轻代和年老代花费的时间,一般会提高应用的效率
    • 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
  3. 较小堆引起的碎片问题
    因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
    • -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
    • -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩


 

堆外内存

堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。
在讲解DirectByteBuffer之前,需要先简单了解两个知识点。
java引用类型,因为DirectByteBuffer是通过虚引用(Phantom Reference)来实现堆外内存的释放的。
PhantomReference 是所有“弱引用”中最弱的引用类型。不同于软引用和弱引用,虚引用无法通过 get() 方法来取得目标对象的强引用从而使用目标对象,观察源码可以发现 get() 被重写为永远返回 null。
那虚引用到底有什么作用?其实虚引用主要被用来 跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否被包裹的对象是否已经被被垃圾回收,从而采取行动。它并不被期待用来取得目标对象的引用,而目标对象被回收后,它的引用会被放入一个 ReferenceQueue 对象中,从而达到跟踪对象垃圾回收的作用。
关于linux的内核态和用户态
因此我们可以得知当我们通过JNI调用的native方法实际上就是从用户态切换到了内核态的一种方式。并且通过该系统调用使用操作系统所提供的功能。
Q:为什么需要用户进程(位于用户态中)要通过系统调用(Java中即使JNI)来调用内核态中的资源,或者说调用操作系统的服务了?
A:intel cpu提供Ring0-Ring3四种级别的运行模式,Ring0级别最高,Ring3最低。Linux使用了Ring3级别运行用户态,Ring0作为内核态。Ring3状态不能访问Ring0的地址空间,包括代码和数据。因此用户态是没有权限去操作内核态的资源的,它只能通过系统调用外完成用户态到内核态的切换,然后在完成相关操作后再有内核态切换回用户态。

DirectByteBuffer ———— 直接缓冲

DirectByteBuffer是Java用于实现堆外内存的一个重要类,我们可以通过该类实现堆外内存的创建、使用和销毁。
DirectByteBuffer该类本身还是位于Java内存模型的堆中。堆内内存是JVM可以直接管控、操纵。
而DirectByteBuffer中的unsafe.allocateMemory(size);是个一个native方法,这个方法分配的是堆外内存,通过C的malloc来进行分配的。分配的内存是系统本地的内存,并不在Java的内存中,也不属于JVM管控范围,所以在DirectByteBuffer一定会存在某种方式来操纵堆外内存。
在DirectByteBuffer的父类Buffer中有个address属性:

1
2
3
// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;

address只会被直接缓存给使用到。之所以将address属性升级放在Buffer中,是为了在JNI调用GetDirectBufferAddress时提升它调用的速率。
address表示分配的堆外内存的地址。
unsafe.allocateMemory(size);分配完堆外内存后就会返回分配的堆外内存基地址,并将这个地址赋值给了address属性。这样我们后面通过JNI对这个堆外内存操作时都是通过这个address来实现的了。
在前面我们说过,在linux中内核态的权限是最高的,那么在内核态的场景下,操作系统是可以访问任何一个内存区域的,所以操作系统是可以访问到Java堆的这个内存区域的。
Q:那为什么操作系统不直接访问Java堆内的内存区域了?
A:这是因为JNI方法访问的内存区域是一个已经确定了的内存区域地质,那么该内存地址指向的是Java堆内内存的话,那么如果在操作系统正在访问这个内存地址的时候,Java在这个时候进行了GC操作,而GC操作会涉及到数据的移动操作[GC经常会进行先标志在压缩的操作。即,将可回收的空间做标志,然后清空标志位置的内存,然后会进行一个压缩,压缩就会涉及到对象的移动,移动的目的是为了腾出一块更加完整、连续的内存空间,以容纳更大的新对象],数据的移动会使JNI调用的数据错乱。所以JNI调用的内存是不能进行GC操作的。
Q:如上面所说,JNI调用的内存是不能进行GC操作的,那该如何解决了?
A:①堆内内存与堆外内存之间数据拷贝的方式(并且在将堆内内存拷贝到堆外内存的过程JVM会保证不会进行GC操作):比如我们要完成一个从文件中读数据到堆内内存的操作,即FileChannelImpl.read(HeapByteBuffer)。这里实际上File I/O会将数据读到堆外内存中,然后堆外内存再讲数据拷贝到堆内内存,这样我们就读到了文件中的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
        if (var1.isReadOnly()) {
            throw new IllegalArgumentException("Read-only buffer");
        } else if (var1 instanceof DirectBuffer) {
            return readIntoNativeBuffer(var0, var1, var2, var4);
        } else {
            // 分配临时的堆外内存
            ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());
 
            int var7;
            try {
                // File I/O 操作会将数据读入到堆外内存中
                int var6 = readIntoNativeBuffer(var0, var5, var2, var4);
                var5.flip();
                if (var6 > 0) {
                    // 将堆外内存的数据拷贝到堆外内存中
                    var1.put(var5);
                }
 
                var7 = var6;
            } finally {
                // 里面会调用DirectBuffer.cleaner().clean()来释放临时的堆外内存
                Util.offerFirstTemporaryDirectBuffer(var5);
            }
 
            return var7;
        }
    }

而写操作则反之,我们会将堆内内存的数据线写到对堆外内存中,然后操作系统会将堆外内存的数据写入到文件中。
② 直接使用堆外内存,如DirectByteBuffer:这种方式是直接在堆外分配一个内存(即,native memory)来存储数据,程序通过JNI直接将数据读/写到堆外内存中。因为数据直接写入到了堆外内存中,所以这种方式就不会再在JVM管控的堆内再分配内存来存储数据了,也就不存在堆内内存和堆外内存数据拷贝的操作了。这样在进行I/O操作时,只需要将这个堆外内存地址传给JNI的I/O的函数就好了。

DirectByteBuffer堆外内存的创建和回收的源码解读

堆外内存分配


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        // 保留总分配内存(按页分配)的大小和实际内存的大小
        Bits.reserveMemory(size, cap);
 
        long base = 0;
        try {
            // 通过unsafe.allocateMemory分配堆外内存,并返回堆外内存的基地址
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        // 构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,堆外内存也会被释放
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

Bits.reserveMemory(size, cap) 方法


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
static void reserveMemory(long size, int cap) {
 
        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }
 
        // optimist!
        if (tryReserveMemory(size, cap)) {
            return;
        }
 
        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
 
        // retry while helping enqueue pending Reference objects
        // which includes executing pending Cleaner(s) which includes
        // Cleaner(s) that free direct buffer memory
        while (jlra.tryHandlePendingReference()) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }
 
        // trigger VM's Reference processing
        System.gc();
 
        // a retry loop with exponential back-off delays
        // (this gives VM some time to do it's job)
        boolean interrupted = false;
        try {
            long sleepTime = 1;
            int sleeps = 0;
            while (true) {
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1;
                        sleeps++;
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }
 
            // no luck
            throw new OutOfMemoryError("Direct buffer memory");
 
        } finally {
            if (interrupted) {
                // don't swallow interrupts
                Thread.currentThread().interrupt();
            }
        }
    }

该方法用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小。
其中,如果系统中内存( 即,堆外内存 )不够的话:

1
2
3
4
5
6
7
8
9
10
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
 
        // retry while helping enqueue pending Reference objects
        // which includes executing pending Cleaner(s) which includes
        // Cleaner(s) that free direct buffer memory
        while (jlra.tryHandlePendingReference()) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

jlra.tryHandlePendingReference()会触发一次非堵塞的Reference#tryHandlePending(false)。该方法会将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。
因为在Reference的静态代码块中定义了:

1
2
3
4
5
6
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });

如果在进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则

1
2
// trigger VM's Reference processing
System.gc();

System.gc()会触发一个full gc,当然前提是你没有显示的设置-XX:+DisableExplicitGC来禁用显式GC。并且你需要知道,调用System.gc()并不能够保证full gc马上就能被执行。
所以在后面打代码中,会进行最多9次尝试,看是否有足够的可用堆外内存来分配堆外内存。并且每次尝试之前,都对延迟等待时间,已给JVM足够的时间去完成full gc操作。如果9次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出OutOfMemoryError(“Direct buffer memory”)异常。
注意,这里之所以用使用full gc的很重要的一个原因是:System.gc()会对新生代的老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及他们关联的堆外内存.
DirectByteBuffer对象本身其实是很小的,但是它后面可能关联了一个非常大的堆外内存,因此我们通常称之为冰山对象.
我们做ygc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,但是无法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是我们通常碰到的最大的问题。( 并且堆外内存多用于生命期中等或较长的对象 )
如果有大量的DirectByteBuffer对象移到了old,但是又一直没有做cms gc或者full gc,而只进行ygc,那么我们的物理内存可能被慢慢耗光,但是我们还不知道发生了什么,因为heap明明剩余的内存还很多(前提是我们禁用了System.gc – JVM参数DisableExplicitGC)。
总的来说,Bits.reserveMemory(size, cap)方法在可用堆外内存不足以分配给当前要创建的堆外内存大小时,会实现以下的步骤来尝试完成本次堆外内存的创建:
① 触发一次非堵塞的Reference#tryHandlePending(false)。该方法会将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。
② 如果进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则进行 System.gc()。System.gc()会触发一个full gc,但你需要知道,调用System.gc()并不能够保证full gc马上就能被执行。所以在后面打代码中,会进行最多9次尝试,看是否有足够的可用堆外内存来分配堆外内存。并且每次尝试之前,都对延迟等待时间,已给JVM足够的时间去完成full gc操作。
注意,如果你设置了-XX:+DisableExplicitGC,将会禁用显示GC,这会使System.gc()调用无效。
③ 如果9次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出OutOfMemoryError(“Direct buffer memory”)异常。
那么可用堆外内存到底是多少了?,即默认堆外存内存有多大:
① 如果我们没有通过-XX:MaxDirectMemorySize来指定最大的堆外内存。则
② 如果我们没通过-Dsun.nio.MaxDirectMemorySize指定了这个属性,且它不等于-1。则
③ 那么最大堆外内存的值来自于directMemory = Runtime.getRuntime().maxMemory(),这是一个native方法

1
2
3
4
5
6
7
8
9
10
11
JNIEXPORT jlong JNICALL
Java_java_lang_Runtime_maxMemory(JNIEnv *env, jobject this)
{
    return JVM_MaxMemory();
}
 
JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void))
  JVMWrapper("JVM_MaxMemory");
  size_t n = Universe::heap()->max_capacity();
  return convert_size_t_to_jlong(n);
JVM_END

其中在我们使用CMS GC的情况下也就是我们设置的-Xmx的值里除去一个survivor的大小就是默认的堆外内存的大小了。

堆外内存回收

Cleaner是PhantomReference的子类,并通过自身的next和prev字段维护的一个双向链表。PhantomReference的作用在于跟踪垃圾回收过程,并不会对对象的垃圾回收过程造成任何的影响。
所以cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 用于对当前构造的DirectByteBuffer对象的垃圾回收过程进行跟踪。
当DirectByteBuffer对象从pending状态 ——> enqueue状态时,会触发Cleaner的clean(),而Cleaner的clean()的方法会实现通过unsafe对堆外内存的释放。
 
虽然Cleaner不会调用到Reference.clear(),但Cleaner的clean()方法调用了remove(this),即将当前Cleaner从Cleaner链表中移除,这样当clean()执行完后,Cleaner就是一个无引用指向的对象了,也就是可被GC回收的对象。
thunk方法:

通过配置参数的方式来回收堆外内存

同时我们可以通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc()来做一次full gc,以此来回收掉没有被使用的堆外内存。

堆外内存那些事

使用堆外内存的原因

因为full gc 意味着彻底回收,彻底回收时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,这意味着一个重要的事实——这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。

什么情况下使用堆外内存

堆外内存 VS 内存池

堆外内存的特点

堆外内存的一些问题

参考




尽管Java隐藏了对内存的直接操作(jvm层面的很多数据都是大端),在JVM层实现了抽象的内存模型和垃圾回收算法,通常Java的程序员不需要了解底层的硬件特性,但是有的时候还是需要知道底层硬件CPU是大端还是小端,首先了解一下什么是大小端。
大小端的主要区别在于低位地址存的是高位还是地位:
大端(Big Endian): 低地址存高位。
小端(Little Endian):低地址存低位。
在JDK的NIO类库中,java.nio.ByteOrder类的方法nativeOrder()提供了判断底层硬件CPU大小端的信息的方法,这个方法是依赖于一段静态代码,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static {
long a = unsafe.allocateMemory(8);
try {
unsafe.putLong(a, 0x0102030405060708L);
byte b = unsafe.getByte(a);
switch (b) {
case 0x01: byteOrder = ByteOrder.BIG_ENDIAN;     break;
case 0x08: byteOrder = ByteOrder.LITTLE_ENDIAN;  break;
default:
assert false;
byteOrder = null;
}
} finally {
unsafe.freeMemory(a);
}
}

可见,java.nio.ByteOrder类的方法nativeOrder()是通过使用unsafe类操作底层JVM的接口来判断大小端信息的。
如果你是一个应用开发者,正好你也需要知道底层硬件CPU的大小端信息,你可以直接使用这个类提供的API来实现:

1
2
3
4
5
6
7
8
9
10
11
import java.nio.ByteOrder;

public class TestCPU {
public static void main(String[] args) {
if (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN) {
System.out.println("BIG_ENDIAN");
} else {
System.out.println("LITTLE_ENDIAN");
}
}
}

此程序, 在Windows Intel上输出的结果是:
=> java TestCPU
LITTLE_ENDIAN
在AIX PPC64上面的输出结果是:
=> java TestCPU
BIG_ENDIAN
事实上,对于底层的硬件操作,C语言实现起来更简单:



使用 HTML5,通过创建 cache manifest 文件,可以轻松地创建 web 应用的离线版本。

什么是应用程序缓存(Application Cache)?

HTML5 引入了应用程序缓存,这意味着 web 应用可进行缓存,并可在没有因特网连接时进行访问。
应用程序缓存为应用带来三个优势:
  • 离线浏览 - 用户可在应用离线时使用它们
  • 速度 - 已缓存资源加载得更快
  • 减少服务器负载 - 浏览器将只从服务器下载更新过或更改过的资源。

浏览器支持

所有主流浏览器均支持应用程序缓存,除了 Internet Explorer。

HTML5 Cache Manifest 实例

下面的例子展示了带有 cache manifest 的 HTML 文档(供离线浏览):

实例

<!DOCTYPE HTML>
<html manifest="demo.appcache">

<body>
The content of the document......
</body>

</html>
亲自试一试

Cache Manifest 基础

如需启用应用程序缓存,请在文档的 <html> 标签中包含 manifest 属性:
<!DOCTYPE HTML>
<html manifest="demo.appcache">
...
</html>
每个指定了 manifest 的页面在用户对其访问时都会被缓存。如果未指定 manifest 属性,则页面不会被缓存(除非在 manifest 文件中直接指定了该页面)。
manifest 文件的建议的文件扩展名是:".appcache"。
请注意,manifest 文件需要配置正确的 MIME-type,即 "text/cache-manifest"。必须在 web 服务器上进行配置。

Manifest 文件

manifest 文件是简单的文本文件,它告知浏览器被缓存的内容(以及不缓存的内容)。
manifest 文件可分为三个部分:

CACHE MANIFEST

第一行,CACHE MANIFEST,是必需的:
CACHE MANIFEST
/theme.css
/logo.gif
/main.js
上面的 manifest 文件列出了三个资源:一个 CSS 文件,一个 GIF 图像,以及一个 JavaScript 文件。当 manifest 文件加载后,浏览器会从网站的根目录下载这三个文件。然后,无论用户何时与因特网断开连接,这些资源依然是可用的。

NETWORK

下面的 NETWORK 小节规定文件 "login.asp" 永远不会被缓存,且离线时是不可用的:
NETWORK:
login.asp
可以使用星号来指示所有其他资源/文件都需要因特网连接:
NETWORK:
*

FALLBACK

下面的 FALLBACK 小节规定如果无法建立因特网连接,则用 "offline.html" 替代 /html5/ 目录中的所有文件:
FALLBACK:
/html5/ /404.html
注释:第一个 URI 是资源,第二个是替补。

更新缓存

一旦应用被缓存,它就会保持缓存直到发生下列情况:

实例 - 完整的 Manifest 文件

CACHE MANIFEST
# 2012-02-21 v1.0.0
/theme.css
/logo.gif
/main.js

NETWORK:
login.asp

FALLBACK:
/html5/ /404.html
重要的提示:以 "#" 开头的是注释行,但也可满足其他用途。应用的缓存会在其 manifest 文件更改时被更新。如果您编辑了一幅图片,或者修改了一个 JavaScript 函数,这些改变都不会被重新缓存。更新注释行中的日期和版本号是一种使浏览器重新缓存文件的办法。

关于应用程序缓存的注释

请留心缓存的内容。
一旦文件被缓存,则浏览器会继续展示已缓存的版本,即使您修改了服务器上的文件。为了确保浏览器更新缓存,您需要更新 manifest 文件。
注释:浏览器对缓存数据的容量限制可能不太一样(某些浏览器设置的限制是每个站点 5MB)。


一.概述
垃圾回收需要完成的三件事情:
        1.哪些内存需要回收
        2.什么时候回收
        3.怎么回收
为什么要了解GC和内存分配?
        1.排查各种内存溢出、内存泄漏问题
        2.当垃圾收集成为系统达到更高并发量的瓶颈时
程序计数器、虚拟机栈和本地方法栈三个区域随线程而生、随线程而灭;栈中的栈帧分配多少内存基本在类结构确定下来时就是已知的(可能会被jit进行优化,大体上可认为)

二.对象已死吗

    1.引用计数法
       给对象添加一个引用计数器(如果让我实现,我就在对象头中拿几个字节干这事),每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减一(计数器值加一可通过重载运算符实现)
       使用语言:微软com,python等
       实现简单,但无法解决循环引用问题,主流jvm虚拟机都没有采用这种算法。

    2.可达性分析算法
        通过一系列的称为“GC Roots”的对象作为起始点,从这些节点往下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(GC Root到这个对象不可达),则证明此对象是不可用的。
       使用语言:java的主流虚拟机、.net、lisp。
       可作为JVM的GC Roots的对象包括下面几种:
       (1)虚拟机栈(栈帧中的本地变量表)中引用的对象。
       (2)方法区中类静态属性引用的对象。
       (3)方法区中常量引用的对象(常量包括字面量和符号引用,这儿可能主要还是值字符串常量)
       (4)本地方法栈JNI(本地方法)引用的对象
  
  3.再谈引用
       jdk1.2后,java对引用的概念进行了扩充,将引用分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference),这四种引用的引用强度依次减弱。
       
          补充:
                 1.弱引用关联的对象并不是只能生存到下一次垃圾收集发生前,而是弱引用不参与对象的引用可达性分析当中,所以应该把这话修改为只被弱引用关联的对象只能生存到下一次垃圾收集发生前。
                 2.虚引用不是拿来存放对象的,而是拿来跟踪垃圾回收的,弱引用关联的对象只能生存到下一次垃圾收集发生前,被回收时该虚引用那个对象(不是被引用那个对象)将会被拿来放在这个queue当中,从而我们可以根据这个queue在程序自运行时了解垃圾回收的部分状况
                        public PhantomReference(T referent, ReferenceQueue<? super T> q) {
                                  super(referent, q);
                         }
   3.生存还是死亡
       即便是在可达性分析算法中不可达的对象,也并非是“必死不可”的,要真正宣告一个对象死亡,至少要经历两次标记阶段。
      第一个标记阶段:标记出引用不可达的对象
      第二个标记 阶段:在上面不可达的对象出,标记出没有必要执行finalize方法的对象(没有重写过该方法或者该方法已经被调用过)。
      
       finalize方法并不等同于C/c++中的析构函数,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,虽然该方法可以进行资源释放和回收工作,但是完全可以使用try-finally或者try-with-resource来做,建议忘掉finalize方法的存在
       
  4.方法区的回收
     java虚拟机规范当中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集性价比一般比较低。在堆中,尤其是新生代,常规引用进行一次垃圾回收一般可以回收70~95%的空间,而永久代远远低于此。
    

     补充:
      tomcat卸载一个web应用就会存在对方法区的垃圾回收。

三.垃圾收集算法
      1.标记-清除算法
         标记的过程就是标记GC-Root无法到达的对象
         
         不足之处:
         1.效率问题,标记和清除的效率都不高
         2.空间问题,会产生大量不连续的内存碎片
      2.复制算法
         将可用内存按容量分为大小相等的两块(可以不只两块),每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上,然后再把已经使用过的内存空间一次清理掉。
        实现简单高效,这种算法的代价是将内存缩小为了原来的一半,浪费太大。不过可以通过分成多个区来改善。
        
        现代商用虚拟机都是采用复制算法来回收新生代,新生代中的对象98%都是朝生夕死的,所以并不需要将新生代对半分(因为经历过一次垃圾收集后回收的内存并不多),Hotspot当中,将新生代分成一块较大的Eden区和两块较小的survivor区(from、to),每次使用Eden和其中一块survivor。当垃圾回收时,将Eden和已经使用的Survivor中还存活着的对象一次性复制到另外一块survivor当中,最后清理掉Eden和刚刚使用过的Survivor空间。
        Hotspot虚拟机默认Eden和survivor的大小比例是8:1(80+10+10),因此新生代中可用内存空间为整个新生代内存空间的90%。棒!
        当然,98%的对象回收只是一般情况,我们没有办法保证每次回收都只有不到10%的对象存活。当Survivor空间不够用时,需要依赖其它内存(老年代)进行分配担保。当Survivor空间不足以容纳新生代中存活的对象时,这些对象将通过分配担保机制进入老年代。
        
      3.标记-整理算法
        复制算法在对象存活率较高时就要进行教多的复制操作,效率将会变低。如果不想浪费50的内存,就必须有额外的空间做内存担保来应对大量对象存活的情况,所以老年代当中一般选用标记-整理算法。   
    
      4.分代收集算法
      根据新生代和老年代的策略使用不同的收集算法和收集策略(收集器)
      新生代因为很多对象朝生夕灭,所以采用复制算法
      老年代因为对象存活时间长,所以采用标记-整理算法
   
四.hotspot的算法实现
    1.枚举根节点
      
           简单的说:
           因为GC root是全局性引用(常量和静态变量)与执行上下文(如栈帧中的局部变量表),那么意味着每次进行GC的时候,需要整个遍历方法区和栈帧,这个遍历的时间开销非常大,好在现代jvm基本都实现了准确式内存管理(jvm知道对应地址上是否是对象),因此使用空间换时间,使用一个oopmap的数据存储结构记录可以作为GC ROOT的对象的引用,每次gc进行可达性分析时,直接在oopmap中取gc root就可以了。
          除非之外:
         什么是GC停顿?GC停顿就是当发生GC时(无论是young gc还是full gc)需要暂停所有运行线程(sun将其称为“stop the world”),目的是为了防止GC时,对象引用发生变化而导致同步问题,
         号称永不停顿的CMS收集器和G1收集器也会有停顿,不过他们只在使用oopmap枚举根节点时发生停顿,当找出哪些对象可以回收后,其它线程就可以继续运行。但其它几款收集器是整个gc过程都停顿。

       2.安全点
        在oopmap的协助下,Hotspot可以快速准确的完成GC-ROOT的枚举,但是有个问题::可能导致引用变化的指令,或者说导致oopmap变化的指令非常多,如果为每条指令都去修改oopmap,那会需要大量额外空间和时间,那么带来的gc成本也会非常高。
       因此hotspot只在特定位置上记录oopmap的信息,这些位置称为安全点,比如方法调用、循环跳转和异常跳转。
       安全点既是记录oopmap的时间点,也是进行gc的时间点,只有当所有线程(不包括执行jni的线程)都执行到安全点并停顿时才进行gc。这儿主要包括抢先式中断和主动式中断两种方法。

       3.安全区域
         安全点只是针对处于处于运行态和就绪态的线程,万一一个线程处于sleep或者blocked,那该线程就无法响应gc的中断请求,从而走到安全点去
挂起,jvm可能也无法一直等待下去,这就需要安全区。
         安全区域是指在一段代码片段之中,引用关系不会发生变化(处于sleep、blocked 、waiting等状态的线程),在这个区域内的任意地方开始GC都是安全的。
补充:线程在进入safe region时肯定要进行一次记录oopmap的操作。

五.垃圾收集器

       java虚拟机规范对垃圾收集器如何实现并没有规定。这里讨论的收集器基于JDK 1.7 Update 14后的Hotspot的垃圾收集器(这个版本正式提供了商用的G1收集器,之前一直处于实验阶段)。
       
          带连线代表连线两边的收集器可以搭配使用,在哪个区域表明它是属于哪个代的收集器(只有G1可以同时拿来回收新生代和老年代)
          没有最棒的收集器,只有最合适的收集器。
      1.Serial收集器
      java3之前是新生代收集器的唯一选择,依靠单个GC线程使用复制算法回收内存,回收期间暂停所有用户线程,一直是client模式下的虚拟机的默认收集器,在单核机器上运行效果最佳。
     Serial垃圾收集器和Serial old收集器运行示意图:
       
    
      2.serial old收集器
          serial收集器的老年代版本,使用标记整理算法。
          
 
      3.parnew收集器
         parnew收集器是serial收集器的多线程版本,除了使用多条线程进行垃圾回收,其余行为:serial收集器的控制参数、收集算法、回收期间暂停所有用户线程(stop the world)、对象分配规则与回收策略与serial收集器一致。
         该收集器在单核机器上其性能因为在回收期间多个gc线程会有上下文切换的开销和内存同步问题,所以哪怕使用超线程技术也无法和serial收集器媲美。但多核机器上是很棒的。
         parnew收集器和serial收集器时唯一能够与CMS收集器搭配的新生代收集器。注意:parnew收集器没有老年代版本。
        
        
      4.parallel scavenge收集器
         和parnew一样,也是使用复制算法和多线程回收新生代的收集器。
         不同点在于parallel scavenge收集器关注的是用户程序的吞吐量(而不是GC停顿时间)和它的自适应调节策略(自适应调节是吞吐量控制的关键)。
         parallel scavenge收集器的目的是达到一个可控制的吞吐量。(这儿说的是关于用户代码运行时间和GC时间的吞吐量,吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间))。
         该收集器提供了两个参数用于精确控制吞吐量:
         -XX:MaxGCPauseMills:控制最大垃圾收集挺顿时间,收集器尽量保证内存回收时间在该时间之内。
        -XX:GCTimeRatio:直接控制吞吐量大小,通过自适应调节策略。
        -XX:+UseAdaptiveSizePolicy:自适应调节开关,打开以后,虚拟机会自动调节新生代的大小、Eden和Survivor的比例、晋升老年代年龄等等,虚拟机会根据目前系统的运行情况收集性能监控信息,动态调整这些参数以提供合适的GC停顿时间和最大的吞吐量。
        新生代越小、GC停顿时间越短、相同时间内的回收次数越多(算上GC停顿缩短的时间,可能吞吐量更低),一个很尴尬的事实。
       
        关注吞吐量的收集器能够提高更好的吞吐量,可以更有效利用cpu,适合计算任务多而交互少的程序。
        关注GC停顿时间能够提供更短的GC停顿时间,可以让客户端或者服务端的停顿时间更少,所以适合交互多的程序。
        
      5.parallel old收集器 
         parallel scavenge收集器的老年代版本,使用标记-整理算法,也是以吞吐量为目标。
    
      6.CMS收集器
        CMS收集器是一种以获取最短回收停顿时间为目标、基于标记清理算法的和使用多线程回收老年代收集器。
        该收集器适合用户交互多的互联网程序,能带来更好的用户体验。
        该收集器是一款并发收集器,能够与用户线程一起工作。
        收集阶段如下:
       1.初始标记:暂停用户线程,标识GC-Roots关联的对象
       2.并发标记:与用户线程一起运行,标识GC-Roots可达的对象
       3.重新标记:暂停用户线程,标记第二步并发标记期间因为用户线程的运行而导致的变化的对象。
       4.并发清除:与用户线程一起运行,并发清除不可达的对象。

       
   CMS收集器有三个缺点:
      1.CMS收集器对CPU十分敏感,虽然不会导致用户线程停顿,但是在用户线程运行期间会因为回收期间占用了一部分CPU用户资源而导致用户程序的CPU资源被剥夺,因为线程切换上下文等影响,实际上吞吐量更低。
         CMS默认使用的收集线程数量是:(CPU数量+三)/4。所以当核数大于等于4核时,只有25%的线程资源被剥夺,所以可以接受。但核数低于4时,影响极大。
      2.CMS收集器无法处理浮动垃圾。浮动垃圾是指CMS收集器在进行第4个并发清除阶段时,用户线程运行又先产生的垃圾。
         这将造成两个后果:
         1.垃圾收集结束后不得不再进行一次垃圾收集
         2.内存直接溢出,所以CMS收集器都不是在内存占用快100%时收集。
         无论如何都有风险,所以CMS收集器处理问题已经无法解决时,将直接启用serial old收集器。
      3.内存碎片 问题,所以CMS收集器在适当的回收次数时,还是会暂停用户线程来进行已使用内存的压缩整理。
      7.G1收集器       
         该收集器是收集器研究的前沿,最早在jdk7u时才可使用,jdk9已经作为默认收集器。
         该收集器的定位是面向服务端的垃圾收集器,使命是为了替换CMS收集器。
         具有以下几个特点:
           1.使用多线程进行垃圾回收,充分利用多cpu或者多核cpu的优势。
           2.分代收集:分代收集的概念在G1收集器中也有保留,不同的策略和回收模式。(唯一一个可对新生代和老年代都进行收集的收集器)
           3.空间整合:G1收集器并不是单纯的垃圾收集算法,而是整理上看起来是标记-整理算法,但局部是复制算法,所以不会产生多少内存碎片
           4.可预测的停顿
         G1虽然还有新生代和老年代的概念,不过实际上已经将堆内存分为了多个大小相等的独立区域,内存布局和我们平时认识的大为不同。
                
    8.垃圾收集器自我总结
       垃圾收集器根据使用目的和场景可以分为以下派别:
       1.Serial收集器/Serial old收集器:单线程的收集器,回收期间,停止所有用户线程。关注停顿时间。
       2.parnew收集器:多线程的收集器,回收期间,停止所有用户线程。关注停顿时间。
       3.parallel scaverage和paranew收集器:多线程的收集器,回收期间,停止所有用户线程。自适应调节策略,关注吞吐量。
       4.CMS收集器和G1收集器:多线程的收集器,回收期间与用户线程并发运行,关注停顿时间。
     因为serial、parnew这两个新生代收集器的新生代内存不会变,所以只有它们能够和CMS收集器一起使用,G1收集器是整个堆内存。
    9.GC日志的理解
       略
    10.垃圾收集器的参数
        
六.内存分配与回收策略
    内存分配主要是指对象在堆内存的分配,但并非都在堆上分配(可能使用TLAB分配模式,或者以后的标量分配在栈上)
  1.什么是Minor GC和Full GC
     注意:G1收集器没有这种概念
     新生代GC(Minor GC):指发生在新生代的垃圾收集动作,非常频繁,一般回收速度较快。
     老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随着至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。

  2.对象优先在Eden区分配
     大多数情况下,对象在新生代Eden中分配,当Eden区没有足够的空间时,虚拟机将发起一次Minor GC。

  3.大对象直接进入老年代
     大对象就是需要占用大量连续内存空间的对象,一般是数组(一个存放含有大量字段的对象的集合不算大对象,想想为什么)。
     虚拟机有一个-XX:PretenureSizeThehold参数可以设置一个阈值,当内存大于这个阈值这个的对象就会直接在老年代中分配内存
     尽量避免短暂周期的大对象。

  4.长期存活的对象进入老年代
      虚拟机给每个对象定义了一个对象年龄计数器(GC分代年龄)
      虚拟机提供-XX:MaxTenuringThrehold参数控制在新生代中存活次数的对象进入老年代。
  5.动态对象年龄分配
     

  6.空间分配担保
     在发生Minor GC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果该条件成立,那么Minor GC是安全的,那就进行Minor GC,如果Minor GC中的survivor存不下就进入老年代。
     如果不成立,虚拟机将会首先查看HandlePromotionFailure设置值是否准许担保失败。如果准许,那么会检查老年代的最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管该次Minor GC有风险(survivor区空间不够存放存活下来的对象)。 如果小于或者不准予担保失败,那这时也要进行一次Full GC。

  7.自我总结
    内存越大、回收次数越少、每次回收的停顿时间越大。
    各种情形,吞吐量优先?停顿时间优先?
    两种并发,GC线程间并发------GC线程与用户线程间并发。

收集器
串行、并行or并发
新生代/老年代
算法
目标
适用场景
Serial
串行
新生代
复制算法
响应速度优先
单CPU环境下的Client模式
Serial Old
串行
老年代
标记-整理
响应速度优先
单CPU环境下的Client模式、CMS的后备预案
ParNew
并行
新生代
复制算法
响应速度优先
多CPU环境时在Server模式下与CMS配合
Parallel Scavenge
并行
新生代
复制算法
吞吐量优先
在后台运算而不需要太多交互的任务
Parallel Old
并行
老年代
标记-整理
吞吐量优先
在后台运算而不需要太多交互的任务
CMS
并发
老年代
标记-清除
响应速度优先
集中在互联网站或B/S系统服务端上的Java应用
G1
并发
both
标记-整理+复制算法
响应速度优先
面向服务端应用,将来替换CMS



综述

sun.misc.Unsafe至少从2004年Java1.4开始就存在于Java中了。在Java9中,为了提高JVM的可维护性,Unsafe和许多其他的东西一起都被作为内部使用类隐藏起来了。但是究竟是什么取代Unsafe不得而知,个人推测会有不止一样来取代它,那么问题来了,到底为什么要使用Unsafe?

做一些Java语言不允许但是又十分有用的事情

很多低级语言中可用的技巧在Java中都是不被允许的。对大多数开发者而言这是件好事,既可以拯救你,也可以拯救你的同事们。同样也使得导入开源代码更容易了,因为你能掌握它们可以造成的最大的灾难上限。或者至少明确你可以不小心失误的界限。如果你尝试地足够努力,你也能造成损害。
那你可能会奇怪,为什么还要去尝试呢?当建立库时,Unsafe中很多(但不是所有)方法都很有用,且有些情况下,除了使用JNI,没有其他方法做同样的事情,即使它可能会更加危险同时也会失去Java的“一次编译,永久运行”的跨平台特性。

对象的反序列化

当使用框架反序列化或者构建对象时,会假设从已存在的对象中重建,你期望使用反射来调用类的设置函数,或者更准确一点是能直接设置内部字段甚至是final字段的函数。问题是你想创建一个对象的实例,但你实际上又不需要构造函数,因为它可能会使问题更加困难而且会有副作用。
1
2
3
4
5
6
7
8
9
10
11
public class A implements Serializable {
    private final int num;
        public A(int num) {
        System.out.println("Hello Mum");
        this.num = num;
    }
 
    public int getNum() {
        return num;
    }
}
在这个类中,应该能够重建和设置final字段,但如果你不得不调用构造函数时,它就可能做一些和反序列化无关的事情。有了这些原因,很多库使用Unsafe创建实例而不是调用构造函数。
1
2
3
Unsafe unsafe = getUnsafe();
Class aClass = A.class;
A a = (A) unsafe.allocateInstance(aClass);
调用allocateInstance函数避免了在我们不需要构造函数的时候却调用它。

线程安全的直接获取内存

Unsafe的另外一个用途是线程安全的获取非堆内存。ByteBuffer函数也能使你安全的获取非堆内存或是DirectMemory,但它不会提供任何线程安全的操作。你在进程间共享数据时使用Unsafe尤其有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import sun.misc.Unsafe;
    import sun.nio.ch.DirectBuffer;
 
    import java.io.File;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.lang.reflect.Field;
    import java.nio.MappedByteBuffer;
    import java.nio.channels.FileChannel;
 
    public class PingPongMapMain {
        public static void main(String... args) throws IOException {
            boolean odd;
            switch (args.length < 1 ? "usage" : args[0].toLowerCase()) {
                case "odd":
                    odd = true;
                    break;
                case "even":
                    odd = false;
                    break;
                default:
                    System.err.println("Usage: java PingPongMain [odd|even]");
                    return;
            }
            int runs = 10000000;
            long start = 0;
            System.out.println("Waiting for the other odd/even");
            File counters = new File(System.getProperty("java.io.tmpdir"), "counters.deleteme");
            counters.deleteOnExit();
 
            try (FileChannel fc = new RandomAccessFile(counters, "rw").getChannel()) {
                MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
                long address = ((DirectBuffer) mbb).address();
                for (int i = -1; i < runs; i++) {
                    for (; ; ) {
                        long value = UNSAFE.getLongVolatile(null, address);
                        boolean isOdd = (value & 1) != 0;
                        if (isOdd != odd)
// wait for the other side.
                            continue;
// make the change atomic, just in case there is more than one odd/even process
                        if (UNSAFE.compareAndSwapLong(null, address, value, value + 1))
                            break;
                    }
                    if (i == 0) {
                        System.out.println("Started");
                        start = System.nanoTime();
                    }
                }
            }
            System.out.printf("... Finished, average ping/pong took %,d ns%n",
                    (System.nanoTime() - start) / runs);
        }
 
        static final Unsafe UNSAFE;
 
        static {
            try {
                Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
                theUnsafe.setAccessible(true);
                UNSAFE = (Unsafe) theUnsafe.get(null);
            } catch (Exception e) {
                throw new AssertionError(e);
            }
        }
    }
当你分别在两个程序,一个输入odd一个输入even,中运行时,可以看到两个进程都是通过持久化共享内存交换数据的。
在每个程序中,将相同的磁盘缓存映射到进程中。内存中实际上只有一份文件的副本存在。这意味着内存可以共享,前提是你使用线程安全的操作,比如volatile变量和CAS操作。(译注:CAS Compare and Swap 无锁算法
在两个进程之间有83ns的往返时间。当考虑到System V IPC(进程间通信)大约需要2500ns,而且用IPC volatile替代persisted内存,算是相当快的了。

Unsafe适合在工作中使用吗?

个人不建议直接使用Unsafe。它远比原生的Java开发所需要的测试多。基于这个原因建议还是使用经过测试的库。如果你只是想自己用Unsafe,建议你最好在一个独立的类库中进行全面的测试。这限制了Unsafe在你的应用程序中的使用方式,但会给你一个更安全的Unsafe。

总结

Unsafe在Java中是很有趣的一个存在,你可以一个人在家里随便玩玩。它也有一些工作的应用程序特别是在写底层库的时候,但总的来说,使用经过测试的Unsafe库比直接用要好。
原文链接: Peter Lawrey 翻译: ImportNew.com - fzr
译文链接: http://www.importnew.com/14511.html
[ 转载请保留原文出处、译者和译文链接。]

一.线程的优势
   1.发挥多处理器的性能
      在多核cpu机器上,使用多线程来处理计算任务,将充分利用cpu资源,从而提高吞吐率。
      在单核cpu机器上,使用多线程来处理含有I/O阻塞的任务,可以充分利用cpu资源(其实含有I/O阻塞,对多核无益,一个就很OK),从而提高吞吐率。
   2.建模的简单些
   3.异步事件的简化处理
      如果能够使用操作系统或者开发包支持的线程数量能够BIO满足需求,为什么要使用NIO呢。
   4.响应更灵敏的用户界面
      将耗时操作放在其他线程进行从而不阻塞UI线程,不多说。

二.线程带来的风险
   1.安全性问题
      安全性的含义是:“永远不会发生糟糕的事情”或者说“程序的行为永远正确,实际上就是说在多线程并发的情况下,程序运行结果不会因为线程的交替执行顺序不同而得出不确定的结果。(如果程序依赖线程特定的执行顺序才能得出正确的结果,那么这种我们成为“Race Condition”,即竞态条件)
      多线程的安全性和三个特性息息相关:
      (1)内存模型的可见性
      (2)程序指令的原子性
      (3)程序指令的有序性

   2.活跃性问题
       活跃性关注的目标是:“某件正确的事情最终会发生”或者说“某个操作不会无法继续执行下去”。
       当线程的某个操作无法继续执行下去时,就会发生活跃性问题:
       注意:这个不一定是无法继续执行下去,应该是长时间无法继续下去。
       对于单线程程序中的活跃性问题主要是无限循环或者无限递归,多线程程序中的活跃性问题主要是“死锁”、“饥饿”以及“活锁”问题。

   3.性能问题
       性能问题和活跃性问题很相关,不过含义更广,主要偏向于程序的行为正确,但是却无法满足性能需求的问题。
       性能问题包括多个方面:
       (1)服务时间过长
       (2)响应不灵敏
       (3)吞吐量过低
       (4)资源消耗率过高
       (5)可伸缩性较低
         ......
        产生性能问题的两个原因:
       (1)太多的线程导致争夺CPU资源剧烈,频繁出现上下文切换操作,从而导致保存和恢复上下文、丢失局部性、CPU时间浪费在线程调度上的问题。
       (2)线程共享数据时,很多时候都使用同步机制保证安全,这些机制往往会抑制编译器的某些优化,使CPU的高速缓存失效,增加共享内存总线的同步流量等

 三.线程无处不在
    java中多线程的几个例子:
    1.JVM启动时,为JVM的内部任务(例如,垃圾收集,终结操作)创建后台线程,并创建主线程来运行main方法。
    2.Timer组件
    3.JSP&Servlet的线程池
    4.RMI的线程池
    5.Swing和awt

原文地址:https://www.jianshu.com/p/0261e6cceb3e

开始之前

最近学习了一下NDK的开发, 就来分享一下.
对一个新鲜事物, 我们先解决的无非就是三件事情: 是什么?为什么?怎么做?.

NDK简介

(英语:native development kit,简称NDK)是一种基于原生程序接口的软件开发工具。通过此工具开发的程序直接以本地语言运行,而非虚拟机。因此只有java等基于虚拟机运行的语言的程序才会有原生开发工具包。[维基百科]
  1. NDK是一系列工具的集合
NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的.
  1. NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。
NDK可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。

那我们为什么要使用呢?

  1. 代码的保护。由于apk的java层代码很容易被反编译,而C/C++库反汇难度较大。
  2. 可以方便地使用现存的开源库。大部分现存的开源库都是用C/C++代码编写的。
  3. 提高程序的执行效率。将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。
  4. 便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用。
上述文字致谢Devin Zhang提供理论支持
是什么和为什么我就先介绍到这儿, 接下来就具体看看如何进行NDK的开发

开发前准备

开发环境

请大家务必升级到AS 2.2以上版本, 因为这个版本升级了很多内容, 详情请见 Android Studio 2.2 正式稳定版发布

NDK和CMake 的下载和安装

大家可以直接打开SDK进行下载和安装
SDK
由于NDK的工具包较大, 大家也可以选择从网站中下载: http://wear.techbrood.com/tools/sdk/ndk/, 选择自己对应的版本使用迅雷等工具下载即可, 不过通过这种方法一定要修改local.properties文件, 在里面添加:
//后面改成自己下载后解压的路径名
ndk.dir=C\:\\Users\\Lulu\\AppData\\Local\\Android\\android-ndk-r13

关于CMake
  1. CMakeList.txt 是脚本文件, 需要指定包含哪些源代码;
  2. 可以写一些条件语句, 实现不同的代码包含
  3. 内部说明:
    add_library 表示编译一个代码库, 内部包含了代码库的名称, 以及源代码有哪些

NDK两种开发模式

  1. ndk-build 形式; Android Studio 2.2之前的模式
  2. CMake 形式: CLion C/C++编辑器; AS2.2之后整合了CLion代码, AS就支持了CMake形式的NDK开发

开始开发

接下来通过几个案例来演示NDK的开发流程

创建工程

  1. 新建工程, 选中Include C++ Support


    Include C++ Support
  2. 一路Next之后, 在最后Finish页面尽量选中图示两项, 这样会给我们包裹一些特定的示例代码, 帮助我们理解和使用


    NDK
  3. 点击Finish, 如果出现图示错误的肯定没有好好看上面的 开发前准备


    ERROR

案例一 实现在C语言中隐藏AppKey等信息

step1: 为了使代码整洁, 咱们新建一个类 NativeHelper, 专门用于访问C语言代码的帮助类 并添加获取Appkey的方法
public class NativeHelper {
static {
// 加载C代码库, 库的名称, 必须是CMakeLists.txt中指定的名称
System.loadLibrary("native-lib");
}

//获取C中隐藏的AppKey
public static native String getAppKey();
}

Note: 一定要添加上面的静态代码块的内容, 否则无法加载C代码库
此时的getAppKey()方法标红, 不用管它, 继续....
step2: 在cpp目录下右击创建C/C++ Source, 选择Type, 并勾选 Create an associated hader, 为保持对应, 名字命名为: com_lulu_ndkdemo_NativeHelper, 此时会出现, 在cpp目录下会出现两个文件, 如图:
cpp
step3: 在CMakeLists.txt中的add_library中添加依关系, 点击同步
src/main/cpp/com_lulu_ndkdemo_NativeHelper.c
add_library
Note:
C代码库生成的名称规则
  1. 如果栈顶代码库名称为 "nh" 那么生成的文件必定是libnh.so
    命名规则: lib库名.so
  2. System.loadLibrary(库名); //此处不能包含前面的lib和后面的.so
step4: 在com_lulu_ndkdemo_NativeHelper.c文件中添加c语言代码
#include <jni.h>JNIEXPORT jstring JNICALL
Java_com_lulu_ndkdemo_NativeHelper_getAppKey(JNIEnv *env, jclass type) {

//测试代码, 没有任何意义
char* app_key = "5465465416948";

//生成 Java 中的字符串对象
//指针的指针
// env <=> JNINativeInterface** C语言
return (*env)->NewStringUTF(env, app_key);
}

step5: 在MainActivity中获取AppKey, 查看结果 -> 成功

String appKey = NativeHelper.getAppKey();
Log.d(TAG, "onCreate: appKey => " + appKey);

Note: Java 调用C/C++代码
  1. 任何一个类的方法, 如果声明了native修饰符, 那么就可以认为是一个C代码;
  1. 可以用对象, 类直接调用
  2. 创建C/C++文件; 如果一个类中有一个native的方法, 那么对应的C方法: Java_包名类名方法名(JNIEnv *env, ...);
  3. 当Java类中包含了native的方法, 那么这个类必须写一个静态初初始化块: System.loadLibrary("库名")

案例二 实现在C语言中打印log

接下来, 只简单介绍核心代码, 不再赘述
step1: 在com_lulu_ndkdemo_NativeHelper.c中添加:
JNIEXPORT void JNICALL
Java_com_lulu_ndkdemo_NativeHelper_printLog(JNIEnv *env, jclass type, jstring str_) {
const char *str = (*env)->GetStringUTFChars(env, str_, 0);
//TODO: 显示Android 的日志
// 调用Android的代码
// 代码需要调用系统的日志库, 这个库已经在 CMakeList.txt添加了e,因此可以直接调用
const char *tag = "NativeHelper";
//jstring -> char*
jboolean b = JNI_FALSE;
const char* txt = (*env)->GetStringUTFChars(env, str_, b);
//打印log日志
__android_log_write(ANDROID_LOG_DEBUG, tag, txt);
//释放string
(*env)->ReleaseStringUTFChars(env, str_, str);
}

step2: NativeHelper中添加
//在C中打印logpublic static native void printLog(String str);
step3: MainActivity中调用
//打印C语言中的Log
NativeHelper.printLog("测试Log");

完整代码

Demo已上传到github上, 欢迎大家Clone

小结

至此, 咱们应该大致了解了一下NDK开发的简单流程, 鄙人菜鸟, 希望抛砖引玉, "引"出更好的文章


作者:changer0
链接:https://www.jianshu.com/p/0261e6cceb3e
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



原文地址:http://mp.weixin.qq.com/s?__biz=MzU0NTAzMzgxMA==&mid=2247484216&idx=1&sn=5feff2489d3f533b8c3f04a2ad201d51&chksm=fb725805cc05d11306aabca0e4141becbfa2d43da397489320fcd04b5e5947f33bb847abf93e&mpshare=1&scene=23&srcid=0202GBjcBN30VNgQ27idk6tZ#rd

大家好,这里是yolo@增长黑盒。很久没跟大家见面了,所以这次我来讲一个比较长的故事:美国潮牌电商巨头Karmaloop由于扩张过度,不幸破产。电商老将扛起CMO大旗,借助数据驱动实现了绝地逆转
CMO是时候升级思维,行使CGO的职责啦!现在,我们就一起来学习下失败的教训和增长的经验 --

1.巨头陨落

1999年,25岁的格雷格.赛尔克(Greg Selkoe)还没有意识到,自己即将改变美国的潮流产业。
这个患有多动症、爱好街舞和涂鸦的小伙子生活并不如意,在大学里主修了非常冷门的人类学专业,还跟父母住在波士顿的房子里【1】。这一年,互联网泡沫空前繁荣,他突然意识到自己该做点什么了。于是,他拉上自己的小伙伴,在家中的地下室成立了一个电商平台:karmaloop.com。格雷格的初衷很简单:提供一个便捷的通道,让所有年轻人都可以买到非常酷的衣服 - 即使身边没有潮牌店,也可以从网上订购。那个时候,“从网站上买东西”还是一件比较罕见的事。格雷格的商业模式也很简单,从当地采购街头潮流服饰(streetwear),把资料挂到网站上,然后邮寄给下单的客户【2】。
(Karmaloop创始人格雷格.赛尔克)
没想到,这生意一干就是4年 - 直到29岁,他还跟妻子在地下室发货。不过,长期对“边缘文化”的推崇,让Karmaloop在嘻哈、DJ、滑板等圈子里声名鹊起。
Karmaloop不断壮大,在2007年进入了鼎盛时期,每年的营收都是成倍增长,连嘻哈巨星Kanye West都为其站台点赞。2013年,Karmaloop终于登上顶峰 - 营业额突破1.27亿美金,远远甩开其它潮牌电商,稳居行业第一,成为了美国最大的潮牌经销平台。2014年,Karmaloop更是在“互联网零售500强”中排名134位。但是,看似一片繁荣的背后,却是1亿美元的银行债务【3】。
(Karmaloop官网)
Karmaloop的街头王国已经显现出崩塌的预兆。从2008年起,从来没有接受过风投的格雷格获得了Comvest等私募机构一大笔融资,有了花不完的钱。被荣誉冲昏头脑的格雷格开始大肆扩张,先后建立了PLNDR(快闪电商)、Brick Harbor(滑板电商)、MissKL(女性高端潮牌)、Boylston Trading Co(男性高端潮牌)等独立的Niche站。更让人无法理解的是,他不顾高管们的反对,筹划了一档名为KarmaloopTV的电视节目,进一步强化自己在潮流界的地位【3】。
另一方面,公司推崇张扬的嘻哈文化,所以允许员工在办公室喝酒、放音乐、养狗。但糟糕的是,这最后演变成大家经常开派对,吸大麻,在办公桌上啪啪啪 - 作为CEO,格雷格却毫不知情
慢慢的,公司赚钱的速度已经远远赶不上烧钱的速度了。结果,Karmaloop所有扩张计划均以失败而告终,仅KarmaloopTV就烧掉了1400万美金,节目竟然没上线过。太平盛世下混乱的员工管理,也让公司经不起逆境的考验。
投资人逐渐对Karmaloop丧失了信心,公司没有了新的资金。为了偿还贷款,Karmaloop不得不采取大降价策略以清理库存,很多高端品牌经常能看到40%以上的折扣!不料,这次壮士断臂不仅没有挽回收入,还激起了众多品牌商的不满,纷纷撤出Karmaloop。
2014年末,Karmaloop背水一战,决定采用成本更低的Drop-shipping模式(即直发模式,自己不囤货,顾客下单后由品牌商直接发货)。理论上,这种模式有很大希望挽回败局,扭亏为盈。万万没想到,由于客服和物流跟不上,顾客们经常收不到货,退不了款。这下可好了,老客户们大失所望,骂声一片,Karmaloop把客户和商家两头都得罪了一遍。
2015年,局面彻底失控,Karmaloop的营业额缩水到原来的一半。一切挽救行动均宣告失败,曾经放出消息要收购Karmaloop的Kanye West也没了声音。格雷格的雄心壮志不得不画上句号 - Karmaloop正式宣告破产。Karmaloop也由此获得了一个十分尴尬的“殊荣”:美国历史上第一大破产电商【3】,仅是欠供应商的货款就高达1900万美元!格雷格本人也因欠款500万美元,至今官司缠身。
最终,投资过Karmaloop的私募机构Comvest花费1300万美元,买下了这个烂摊子 - 昔日的巨头被迫以一个月的营业额把自己卖了!

2.新官上任

Comvest接手后,第一件事就是踢掉格雷格这个CEO,并迅速邀请了时尚行业资深人士赛斯.哈勃(Seth Haber)掌管公司大权。但是,作为一家电商公司,营销的重要性自然不用多说 - Karmaloop需要一名给力的CMO,帮助他们穿过泥泞。
很快,Comvest物色到了绝佳人选:今天故事的主角杜鲁.萨诺科齐(Drew Sanocki)。他在电商零售行业也是一位传奇人物:2003年,杜鲁成立了自己的平台Design Public,以纯Dropship的形式出售高档家具,并在第一年就做到了百万美金的营收。2011年,当Karmaloop还在茁壮成长的时候,杜鲁就将自己的公司高价卖给了一家私募公司。从此之后,财务自由的他决定退居二线,成为了一家私募的合伙人 - 主要负责评估零售电商项目,以及投后顾问。
他对零售业营销有着深刻的见解。在Design Public成立之初,他就敏锐地意识到搜索引擎将成为互联网流量的入口,着手打造了一套自动化的SEO体系,成功抢占先机。面对竞争对手的追击,他竟然雇了一名喜剧作家为自己的产品撰写说明书 - 每篇出价高达数百美金!这些高质量的内容打的同行们措手不及【4】。
在Comvest的盛情邀请下,杜鲁决定接受这个挑战- 临危受命,出任Karmaloop的CMO
Karmaloop一直拥有着顶尖的营销能力。10年前,在“增长黑客”这个概念还未诞生,格雷格就开始尝试“0成本指数增长”的策略。那时纸媒还占据着市场的话语权,而Karmaloop却创作了大量街头艺术家、嘻哈音乐人的专访,并通过博客、邮件推送等方式进行分发,几乎没有花钱就获得了大量用户,营收直线上升。
但是,杜鲁开始着手公司业务时,Karmaloop每个月正在流失近百万美金 。
(Karmaloop的流量和营收曲线急剧下滑【5】)
他发现,公司遭遇困境的主要原因就是错误地理解了“增长黑客”的体系:只关注用户获取,而不重视留存和变现 - 大批的用户点水而过,没有给公司创造任何价值
2008年拿了一大笔投资之后,VC急切渴望看到成绩。因此,格雷格决定抄捷径:花钱买流量。正如前文提到的,不论是做电视节目还是开设新的分支品牌,都是为了占据流量入口。除此之外,Karmaloop还投入大量人力物力去做Google Adwords、PR、线下店、纸媒等等。这个时候,没人意识到虚荣的流量其实未曾带来收入。
不过,由于用力过猛,钱都烧光了,公司开始负债。银行还在天天催着还钱,Karmaloop才被迫想办法提升留存,促进变现。
但是,心急的格雷格干脆一刀切:使用打折促销作为激活用户的手段,直接推动留存和变现。这其实是完全错误的,因为用打折的方式吸引的用户必然是消费能力不高、生命周期价值很低。这让本来就不理想的留存率进一步恶化,获得的营收根本抵不上广告投入,还把供应商得罪了。
最后, 为了节约开支,提高变现环节的利润,Karmaloop才决定采用Dropship直发模式。不过,这也是反其道而行之 - 大批高价值的老客户流失,原本固定的收入来源也惨遭冲击。
这让Karmloop陷入了恶性循环 - 不断烧钱,低质量的新客户在增加,高质量的老客户在减少,越来越亏
杜鲁意识到,要想扭转局势,就必须获取更多高价值的客户,并且留住他们,最终让他们带来足够的收入

3.用户获取

面对一团乱麻,杜鲁遭遇的一个问题就是:高价值的客户从哪里来?
3.1.转化低价值用户
很显然,最实际的方案就是把之前的低价值用户转化为高价值用户(或者说筛选出潜在的高价值用户)。为此,他尝试了自己践行多年的“拌网策略”【10】-这分为三个步骤:
1.对高价值客户行为进行数据建模
2.找到实际数据与理想数据的偏差(即“拌网”)
3.集中营销工作的时间和精力,纠正这些偏差
杜鲁自己举过一个例子:
他每个周五都要去一家名叫Slow Burn的健身房锻炼身体 - 对于健身房来说,他就是理想客户,“每周五都来健身”就是所谓的标准模型
但是,过了几个月,他不想去了。可能的原因有三个:
1.自己找到了另一家更好的健身房
2.觉得价格太高了(可能性比较低)
3.自己变懒了(可能性比较高)
“停止去健身”就是所谓的偏差(拌网)。
Slow Burn注意到了这个情况,开始一系列营销活动:提醒他克制懒惰,并送了优惠券给他!
他收到了优惠券,并为自己的懒惰感到惭愧,于是又重新回到了健身房 - 偏差就这样被纠正了
这个理论的核心就是,公司不应该在所有用户上投入相同的营销成本,而是找到那些阻碍普通用户成为高价值用户的“拌网”,然后投入最大精力去解决这个障碍
从入职第一天起,他就花费了整整一个月去研究Karmaloop近10年的交易数据,并利用RFM模型进行了分析。按照他的理论,首先要找出这些“高价值”的用户行为,然后不断鼓励这些行为,就可以把低价值的用户转化为高价值的用户
他创建了两个简单的用户分层【6】:
鲸鱼:多次复购,消费额高,很少退货。代表高价值、高LTV的用户层。
鲦鱼:仅购买一次,只买便宜的商品,而且退货率高。代表低质量、低LTV的用户层,如果算上各种成本,这个分层其实在给公司亏钱。
杜鲁惊讶的发现,“鲸鱼”们只占了1.3%的访问量,却贡献了43%的收入!这些历史数据让Karmaloop的问题暴露无遗:鲦鱼太多而鲸鱼太少,所以才一直亏钱
接下来,就是寻找“鲸鱼”们的共性行为了 - 他非常想知道这些高价值用户究竟花多久来完成第二次购买。杜鲁用两个简单的步骤分析近期订单数据:
1.选中所有满足“鲸鱼”特征的用户
2.计算第一次和第二次购买之间平均间隔天数
分析结果如下图,绿色的条即是杜鲁想要观察的指标。
(X轴代表间隔时长,Y周代表该有多少用户花费了这个间隔去复购)
他发现,80%的情况下,如果一个用户要下单两次,他们都会在第一个订单之后的30天内完成第二单
没错,这就是杜鲁找到的“拌网“ - 大多数“鲦鱼”并不会在30天的周期内产生复购行为。前面的数据分析表明,如果一个用户在第一天下了单,但后续的30天内没有下第二单,那他就越来越不可能成为“鲸鱼”了。但是,如果他们在30内下了单(符合理想用户行为),那就有很大机会成为“鲸鱼”。
Karmaloop需要做的就是使用合理的营销方式与这些”鲦鱼“用户层进行沟通,引导他们在30天下第二个订单,逐步让他们转变为“鲸鱼”
根据上一步的分析,他制定了两种营销策略:
1.在30天内 -> 用户有机会完成二次复购 ->给用户推销原价(高利润)的产品
2.在30天之后->用户越来越难完成二次复购->用大额优惠(低利润)刺激用户
看到这里,大家不难理解为何Karmaloop为何会亏损了:既然30天之内用户有很大概率会复购,那为何还要给他们优惠呢?不重视精细化运营的代价是惨重的。
作为CMO,杜鲁的第一项行动就是不再给30天周期内的用户发送优惠券。另一方面,超过30天后,用户复购的几率依旧随着时间的延长而衰减。因此,杜鲁给优惠券设定了一个梯度 - 间隔时间越长,优惠额度越大
所以,完整的流程就是:
1.用户首次购买后若处于30天周期内,则给他推送原价商品
2.在30天之后还未复购,则给用户推送10%折扣券
3.如果超过了45天,给用户推送20%折扣券
4.如果超过了60天,一律推送30%折扣券
5.如果用户在任意阶段产生了复购行为,则不会触发后续的优惠
6.时间周期和优惠额度会根据数据反馈进行调整
计划制定好了,如何把营销信息推送给客户呢?在海外市场,Email的地位相当于国内的公众号,而Karmaloop凭借十几年的积累已经有了数百万Email订阅用户 - 杜鲁决定利用这些先天的优势,将Email作为营销的主战场。在团队的帮助下,他建立起了基于CRM和Klaviyo软件的自动化邮件系统。
(图中为调整后的优惠梯度邮件)
当邮件取得良好的效果后,营销团队会迅速把活动更新到网站主页、Facebook定向广告、甚至是邮寄的贺卡中
3.2.社交媒体
现在,一部分有潜力的“鲦鱼”已经能够被转化为“鲸鱼”了。但是,比起之前的损失来说,目前的营收增长还远远不够
正如增长黑盒在之前文章中提到的,社交媒体是时尚行业的必争之地(参考:Daniel Wellington案例Supreme案例)。所以,接下来杜鲁开始寻找新的社交媒体渠道,进一步获取高价值客户。
1.首先是Instagram。根据杜鲁从Shopify内部得到的消息,在所有营收超过百万美金的Shopify店铺中,90%都在依赖Instagram做营销【7】。因此,杜鲁也想在Karmaloop身上尝试一下。他从团队中挑选了一位十分有干劲的小伙子,专门负责Instagram等平台的运营,不断寻求KOL合作机会。
(比如这位打call的妹子就有430万Ins粉丝!)
2.其次是Youtube。在公司倒闭之前,格雷格的KarmaloopTV项目拍摄了大量原创视频,都是放在youtube频道中。但是,现在既然没钱了,这个频道肯定也就开不下去了。于是,杜鲁放弃原创视频的想法,转而借助KOL的力量。在Youtube上,有许多被称为“hauler”的网红:他们经常拍摄潮牌的开箱评测,讲解服饰穿搭 - 自然,这些网红的粉丝一定是Karmaloop的精准客户了。杜鲁立刻安排团队拿下了大批hauler,把Karmaloop售卖的招牌产品交给他们去评测,并且跟他们联合举办抽奖活动 - 关注Instagram,留言送潮牌!
(这位花臂小哥也是潮流界名人)

4.激活留存

高价值用户的增长仅仅是开端。接下来,还有更重要的一步:激活并留住他们
这时,杜鲁应用了生命周期营销策略,即在正确的时间,把正确的信息传递给正确的人。
经过分析,他构建起了一个客户的生命周期流程:
(可以看到,任何客户都会经历新手期,活跃期到流失期三个阶段)
借助自动化的邮件系统,杜鲁让营销邮件贯穿每个客户的生命周期。从获取客户开始,5-6个邮件campaign就开始运行了。无论用户进行到哪个时间节点,都能通过邮件接收到最恰当的营销信息
1.新手期:这个阶段的营销目标是引导用户产生第一次购买。
当用户注册/订阅邮件后,立刻启动“欢迎系列邮件” - 即利用5-7封邮件逐步建立品牌信任度、传递Karmaloop的价值、宣传部分招牌产品。
通过欢迎阶段后,许多用户已经准备好要剁手了。但是,从建立信任到最终付款之间,还有一个“隐形杀手”:放弃购物车(cart abandonment)。2016年的数据显示,全球电商的平均弃车率高达77%!【8】因此,杜鲁特意设定了一系列邮件,唤回那些弃车的用户:
2.活跃期:这个阶段的营销目标是让用户保持活跃,持续消费。
怎样才能让客户不断来关注Karmaloop呢?杜鲁的团队创建了一个VIP计划:如果某个用户的消费行为接近“鲸鱼”(举例来说,如果他下了单,而且其平均订单价值(AOV)或者平均订单数量超过某个固定值),就把他定义为VIP用户 - 随后会触发相应的营销活动(比如特殊折扣),而且用户会收到一封感谢邮件。
3.流失期:这个阶段营销目的是唤回流失的老用户。
事实上,大部分Karmaloop的老用户依然停留在这个阶段。公司在倒闭时的垂死挣扎,很大程度上影响了他们的体验,导致信任度大减。
既然这些老用户都在邮件订阅列表上,继续用邮件唤回他们不是很简单吗?不过,杜鲁马上意识到自己低估这件事的难度 - 最初的唤回成功率相当低。当你失去一个人的信任之后,就很难争取第二次机会了
碰壁之后,杜鲁马上启用了增长黑客的秘诀- A/B测试。他将流失的老用户群划分成许多个10000人的小组,逐步测试不同的方案。
他们首先尝试了10-30%范围内的折扣,随后又试了下次购买返现金,紧接着是送礼品卡,最后还试了“CEO亲笔信”,甚至是打电话....
进过不懈的努力,在经历过20多次失败的测试后,杜鲁终于找到了一套最佳组合,老客户们渐渐认可了新生的Karmaloop。
为了能够在整个生命周期内更好的理解高价值用户的行为,杜鲁的团队还利用SurveyMonkey进行了NPS问卷,即询问用户“你有多大意愿把Karmaloop推荐给朋友(1分到10分)?”另外,问卷还包括几个核心问题:
1.你还在其它什么地方购物?
2.哪些商品我们应该卖却没有卖?
3.你平时都阅读哪些博客?
正是通过不断的学习和理解,杜鲁对客户的需求有了更加精准的把握,大大改进了Karmaloop的用户体验和客户服务。
在之前的运营中,Karmaloop根本没有进行任何客户生命周期的营销,这无疑是巨大的突破

5.提高营收

在杜鲁的推动下,Karmaloop终于能够把高价值客户留存下来了。不过,要形成完整的增长闭环,还差最后的盈利阶段:把留下来的客户转化为收入
由于之前的Dropship模式宣告失败,所以Karmaloop还要回归到传统的备货模式。因此,想通过压缩成本来提高利润的方法是行不通了。所以,杜鲁设法提高每用户平均收入(ARPU)
5.1.提高商品单价
事实上,Karmaloop之前的打折策略是严重违背其商业价值的:潮牌本身并没有太多物质上的价值,更多的是精神/文化层面的追求和认可 - 用时髦点的话来说,就是“共识”。比如Supreme从来不打折,但官网的新品总是在几十秒内售罄。
他相信,经过一番调整,Karmaloop现有的用户并不是冲着廉价而来的,而是认可潮牌的价值。那么,顾客到底愿意花多少钱买Karmaloop的产品呢?
杜鲁继续发扬增长黑客的精神,开始了价格实验,针对不同商品供需关系,分组后逐步提价,然后观察用户的购买行为。
他首先把注意力放到了Karmaloop最热销的打底T恤上。经过一段时间提价,他惊讶的发现:价格提高30%后并不会影响销量!也就是说,这个品类的收入足足增加了30%!
5.2.交叉销售
数据显示,亚马逊35%的收入都来自于交叉销售(cross-sell)【9】。在Karmaloop,杜鲁开始实践交叉销售 - 简单来说,就是向购买过本公司A产品的客户推销本公司B产品。从心理学的角度上来说,刚刚完成一次购买的用户正处于“购物期”,心理防线非常薄弱,难以抵抗二次诱惑。借助邮件营销,Karmaloop得以大大提升客户的生命周期价值。
(向购买过打底T恤的人推荐其它颜色的款式)

6.绝地逆转

不到3个月时间,杜鲁就取得了卓越的成效,把公司营收提高了30%,客户生命周期营销活动的ROI高达500%!
最终,仅用了10个月时间,杜鲁就将Karmaloop扭亏为盈
1年半后,Karmaloop以数千万美金的价格出售给了美国球鞋零售商Shiekh Shoes【11】 - 本该从历史上抹去的巨头再次焕发了活力,实现了真正的绝地逆转。
当然,功劳不仅是杜鲁一人的。这个过程中,Karmaloop的CEO也起到了重要作用,包括与品牌方谈判,整顿公司管理,调整供应链等等。另外,杜鲁手下还有一票得力干将,包括一名邮件营销的工程师,一个设计创意团队 - 以及整个产品部门的支援。

总结

这个案例为我们清晰地展示了一位CMO如何利用数据来做决策,科学推动公司的增长和提高 - 其实这些都是首席增长官CGO的职责。看来,可口可乐用CGO取代CMO,并非炒作,而是切实的需求所迫。CMO如何避免被取代呢?当然是秉承精细化运营的思路,坚持数据+技术驱动增长

总结一下,杜鲁究竟做了哪些事让Karmaloop起死回生:
当然,我们能够从Karmaloop的身上吸取不少教训
1.精细化运营决定成败。我们可以发现,Karmaloop之前从来不做客户分层,不分析客户生命周期,也没有自动化营销的概念。因为它依靠着互联网早期的红利起家,不懂得流量的珍贵 - 这与国内目前的形势相似,红利褪去后,野蛮生长需要转变成数据驱动。
2.增长是一个完整的体系。获客、留存、变现是不可割裂的。Karmaloop只关注获客,追求短期的爆发,而忽视了长期的价值积累,必然导致雪崩式的效应。
3.靠打折增长用户是不可取的。不得不说,利用人们贪便宜的心理是一个捷径。但是这必然导致用户本身的质量很低,难以导入后续的留存和变现环节。更重要的是,一旦你开始打折,你就再也无法回头,陷入恶性循环。
4.把一件事做到最好。Karmaloop失败的很大原因就是精力过于分散,急于建设各种周边品牌。说到底,都是为了增加品牌的影响力,但过于“曲线救国”。不如实际一点,比如把潮牌的SEO都做到第一位,或者把Instagram粉丝做到100万。
5.Dropship坑太多,没有经验不要轻易尝试。作为一个过来人,这是我个人的建议。
同时,我们也能从这位CMO身上学到很多东西:
1.增长是整个团队配合的结果。国外有调查显示,增长黑客们的主要阻力来自老板。所以,增长永远都是自上而下的,全员all in的。试想一下,杜鲁就算水平再高,如果CEO不支持,员工很懒惰,还能取得这样的成绩吗?
2.勇于试错。公司的管理层往往都有存量的顾虑。但对于危在旦夕的Karmaloop来说,杜鲁刚好能够抛弃这种负担,从营销活动到产品价格,大胆进行各种实验 - 这也是增长黑客的精神所在。
3.重视VIP用户的运营。二八效应在电商行业体现的尤为明显 - 20%的人贡献了80%的收入。然而,我们大多数时候却把这20%的“金主”当做普通人来对待,这显然是不科学的。我们必须甄别出这些高价值用户,用不同的策略加以培养,从他们身上挖掘更多潜在消费价值。
4.重视数据和技术。如果说数据是灵魂,那么技术就是肉体。在运营Karmaloop的过程中,他都是亲自对数据进行整理和计算,而不是让手下人送个报表过来。另一方面,他非常注重自动化营销 - Karmaloop所有邮件营销都是根据客户行为自动触发的。如果没有这套系统,杜鲁的想法恐怕很难实现。



增长黑盒由Alan&Yolo两人打造,专注于分享增长黑客。我们在伦敦帝国理工读
完了生物学硕士,但是发现真正需要实验思维的地方却是商业战场,于是秉承着学医救不了中国人的信念踏入了自媒体领域。

下面是我们新成立的增长黑客社区,旨在为产品经理、程序员、营销人、运营人、创业者提供一个平台,共同研究增长黑客策略,把科研精神带入商业领域,撬动低成本指数级的增长!欢迎大家一起加入!
知识星球
增长黑客成长社区
小程序
参考资料:
【1】http://nextshark.com/karmaloop-ceo-greg-selkoe-interview/
【2】https://harvardmagazine.com/2012/12/karmaloop
【3】http://www.complex.com/style/the-rise-and-fall-of-karmaloop
【4】https://growtheverywhere.com/growth-everywhere-interview/drew-sanocki-nerd-marketing/
【5】https://www.appcues.com/blog/growing-a-company-with-rapid-growth-strategies
【6】https://speakerdeck.com/wooconf/drew-sanocki-the-top-growth-secrets-of-9-figure-ecommerce-retailers-all-of-which-you-can-steal
【7】http://rejoiner.com/resources/data-driven-marketing-helped-karmaloop-com-claw-way-back-bankruptcy/
【8】https://www.barilliance.com/cart-abandonment-rate-statistics/
【9】https://www.forbes.com/sites/chuckcohn/2015/05/15/a-beginners-guide-to-upselling-and-cross-selling
【10】https://conversionxl.com/blog/tripwire-marketing/
【11】https://www.bostonglobe.com/business/2016/03/23/karmaloop-acquired-california-retailer


原文地址:http://mp.weixin.qq.com/s?__biz=MzUzMTA2NTU2Ng==&mid=2247484038&idx=1&sn=fb9b8f91146eed545a5464bd85675d15&chksm=fa497d37cd3ef4212df9d16cd685273c01ba992f59ef50b838970004153ca4364fd163eb7c06&mpshare=1&scene=23&srcid=02062dwHYCBGoREjfI5tmLFA#rd
本文主要基于 TCC-Transaction 1.2.3.3 正式版

友情提示:欢迎关注公众号【芋道源码】。😈关注后,拉你进【源码圈】微信群和【芋艿】搞基嗨皮。
友情提示:欢迎关注公众号【芋道源码】。😈关注后,拉你进【源码圈】微信群和【芋艿】】搞基嗨皮。
友情提示:欢迎关注公众号【芋道源码】。😈关注后,拉你进【源码圈】微信群和【芋艿】】搞基嗨皮。

1. 概述

本文分享 TCC 实现。主要涉及如下三个 Maven 项目:
你行好事会因为得到赞赏而愉悦 
同理,开源项目贡献者会因为 Star 而更加有动力 
为 TCC-Transaction 点赞!传送门
OK,开始我们的第一段 TCC 旅程吧。
ps:笔者假设你已经阅读过《tcc-transaction 官方文档 —— 使用指南1.2.x》。
ps2:未特殊说明的情况下,本文事务指的是 TCC事务

2. TCC 原理

FROM https://support.hwclouds.com/devg-servicestage/zh-cn_topic_0056814426.html
TCC事务 
为了解决在事务运行过程中大颗粒度资源锁定的问题,业界提出一种新的事务模型,它是基于业务层面的事务定义。锁粒度完全由业务自己控制。它本质是一种补偿的思路。它把事务运行过程分成 Try、Confirm / Cancel 两个阶段。在每个阶段的逻辑由业务代码控制。这样就事务的锁粒度可以完全自由控制。业务可以在牺牲隔离性的情况下,获取更高的性能。
整体流程如下图:
与 2PC协议 比较
参考资料:

3. TCC-Transaction 原理

在 TCC 里,一个业务活动可以有多个事务,每个业务操作归属于不同的事务,即一个事务可以包含多个业务操作。TCC-Transaction 将每个业务操作抽象成事务参与者,每个事务可以包含多个参与者
参与者需要声明 try / confirm / cancel 三个类型的方法,和 TCC 的操作一一对应。在程序里,通过 @Compensable 注解标记在 try 方法上,并填写对应的 confirm / cancel 方法,示例代码如下:
// try
@Compensable(confirmMethod = "confirmRecord", cancelMethod = "cancelRecord", transactionContextEditor = MethodTransactionContextEditor.class)
public String record(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}

// confirm
public void confirmRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}

// cancel
public void cancelRecord(TransactionContext transactionContext, CapitalTradeOrderDto tradeOrderDto) {}
TCC-Transaction 有两个拦截器,通过对 @Compensable AOP 切面( 参与者 try 方法 )进行拦截,透明化对参与者 confirm / cancel 方法调用,从而实现 TCC 。简化流程如下图:
第一个拦截器,可补偿事务拦截器,实现如下功能:
第二个拦截器,资源协调者拦截器,实现如下功能:
实际拦截器对事务的处理会比上图复杂一些,在本文「6. 事务拦截器」详细解析。
在 TCC-Transaction 代码实现上,组件分层如下图:
本文按照如下顺序分享:
内容是自下而上的方式分享,每个组件可以更加整体的被认识。当然这可能对你理解时产生一脸闷逼,所以推荐两种阅读方式:
事务存储器在《TCC-Transaction 源码解析 —— 事务存储于恢复》详细解析。
事务恢复在《TCC-Transaction 源码解析 —— 事务恢复》详细解析。

4. 事务与参与者

在 TCC 里,一个事务( org.mengyun.tcctransaction.Transaction ) 可以有多个参与者( org.mengyun.tcctransaction.Participant )参与业务活动。类图关系如下( 打开大图 ):

4.1 事务

Transaction 实现代码如下
public class Transaction implements Serializable {

private static final long serialVersionUID = 7291423944314337931L;

/**
* 事务编号
*/
private TransactionXid xid;
/**
* 事务状态
*/
private TransactionStatus status;
/**
* 事务类型
*/
private TransactionType transactionType;
/**
* 重试次数
*/
private volatile int retriedCount = 0;
/**
* 创建时间
*/
private Date createTime = new Date();
/**
* 最后更新时间
*/
private Date lastUpdateTime = new Date();
/**
* 版本号
*/
private long version = 1;
/**
* 参与者集合
*/
private List<Participant> participants = new ArrayList<Participant>();
/**
* 附带属性映射
*/
private Map<String, Object> attachments = new ConcurrentHashMap<String, Object>();

/**
* 添加参与者
*
* @param participant 参与者
*/
public void enlistParticipant(Participant participant) {
participants.add(participant);
}

/**
* 提交 TCC 事务
*/
public void commit() {
for (Participant participant : participants) {
participant.commit();
}
}

/**
* 回滚 TCC 事务
*/
public void rollback() {
for (Participant participant : participants) {
participant.rollback();
}
}
}

4.2 参与者

Participant 实现代码如下
public class Participant implements Serializable {

private static final long serialVersionUID = 4127729421281425247L;

/**
* 事务编号
*/
private TransactionXid xid;
/**
* 确认执行业务方法调用上下文
*/
private InvocationContext confirmInvocationContext;
/**
* 取消执行业务方法
*/
private InvocationContext cancelInvocationContext;
/**
* 执行器
*/
private Terminator terminator = new Terminator();
/**
* 事务上下文编辑
*/
Class<? extends TransactionContextEditor> transactionContextEditorClass;

/**
* 提交事务
*/
public void commit() {
terminator.invoke(new TransactionContext(xid, TransactionStatus.CONFIRMING.getId()), confirmInvocationContext, transactionContextEditorClass);
}

/**
* 回滚事务
*/
public void rollback() {
terminator.invoke(new TransactionContext(xid, TransactionStatus.CANCELLING.getId()), cancelInvocationContext, transactionContextEditorClass);
}
}

5. 事务管理器

org.mengyun.tcctransaction.TransactionManager,事务管理器,提供事务的获取、发起、提交、回滚,参与者的新增等等方法。

5.1 发起根事务

提供 begin() 方法,发起根事务。该方法在调用方法类型为 MethodType.ROOT 并且 事务处于 Try 阶段被调用。MethodType 在「6.2 可补偿事务拦截器」详细解析。
实现代码如下:
// TransactionManager.java
/**
* 发起根事务
*
* @return 事务
*/
public Transaction begin() {
// 创建 根事务
Transaction transaction = new Transaction(TransactionType.ROOT);
// 存储 事务
transactionRepository.create(transaction);
// 注册 事务
registerTransaction(transaction);
return transaction;
}

5.2 传播发起分支事务

调用 #propagationNewBegin(...) 方法,传播发起分支事务。该方法在调用方法类型为 MethodType.PROVIDER 并且 事务处于 Try 阶段被调用。MethodType 在「6.2 可补偿事务拦截器」详细解析。
实现代码如下:
/**
* 传播发起分支事务
*
* @param transactionContext 事务上下文
* @return 分支事务
*/
public Transaction propagationNewBegin(TransactionContext transactionContext) {
// 创建 分支事务
Transaction transaction = new Transaction(transactionContext);
// 存储 事务
transactionRepository.create(transaction);
// 注册 事务
registerTransaction(transaction);
return transaction;
}

5.3 传播获取分支事务

调用 #propagationExistBegin(...) 方法,传播发起分支事务。该方法在调用方法类型为 MethodType.PROVIDER 并且 事务处于 Confirm / Cancel 阶段被调用。MethodType 在「6.2 可补偿事务拦截器」详细解析。
实现代码如下:
/**
* 传播获取分支事务
*
* @param transactionContext 事务上下文
* @return 分支事务
* @throws NoExistedTransactionException 当事务不存在时
*/
public Transaction propagationExistBegin(TransactionContext transactionContext) throws NoExistedTransactionException {
// 查询 事务
Transaction transaction = transactionRepository.findByXid(transactionContext.getXid());
if (transaction != null) {
// 设置 事务 状态
transaction.changeStatus(TransactionStatus.valueOf(transactionContext.getStatus()));
// 注册 事务
registerTransaction(transaction);
return transaction;
} else {
throw new NoExistedTransactionException();
}
}

5.4 提交事务

调用 #commit(...) 方法,提交事务。该方法在事务处于 Confirm / Cancel 阶段被调用。
实现代码如下:
/**
* 提交事务
*/
public void commit() {
// 获取 事务
Transaction transaction = getCurrentTransaction();
// 设置 事务状态 为 CONFIRMING
transaction.changeStatus(TransactionStatus.CONFIRMING);
// 更新 事务
transactionRepository.update(transaction);
try {
// 提交 事务
transaction.commit();
// 删除 事务
transactionRepository.delete(transaction);
} catch (Throwable commitException) {
logger.error("compensable transaction confirm failed.", commitException);
throw new ConfirmingException(commitException);
}
}

5.5 回滚事务

调用 #rollback(...) 方法,取消事务,和 #commit() 方法基本类似。该方法在事务处于 Confirm / Cancel 阶段被调用。
实现代码如下:
/**
* 回滚事务
*/
public void rollback() {
// 获取 事务
Transaction transaction = getCurrentTransaction();
// 设置 事务状态 为 CANCELLING
transaction.changeStatus(TransactionStatus.CANCELLING);
// 更新 事务
transactionRepository.update(transaction);
try {
// 提交 事务
transaction.rollback();
// 删除 事务
transactionRepository.delete(transaction);
} catch (Throwable rollbackException) {
logger.error("compensable transaction rollback failed.", rollbackException);
throw new CancellingException(rollbackException);
}
}

5.6 添加参与者到事务

调用 #enlistParticipant(...) 方法,添加参与者到事务。该方法在事务处于 Try 阶段被调用,在「6.3 资源协调者拦截器」有详细解析。
实现代码如下:
/**
* 添加参与者到事务
*
* @param participant 参与者
*/
public void enlistParticipant(Participant participant) {
// 获取 事务
Transaction transaction = this.getCurrentTransaction();
// 添加参与者
transaction.enlistParticipant(participant);
// 更新 事务
transactionRepository.update(transaction);
}

6. 事务拦截器

TCC-Transaction 基于 org.mengyun.tcctransaction.api.@Compensable + org.aspectj.lang.annotation.@Aspect 注解 AOP 切面实现业务方法的 TCC 事务声明拦截,同 Spring 的 org.springframework.transaction.annotation.@Transactional 的实现。
TCC-Transaction 有两个拦截器:
在分享拦截器的实现之前,我们先一起看看 @Compensable 注解。

6.1 Compensable

@Compensable,标记可补偿的方法注解。实现代码如下:
public @interface Compensable {

/**
* 传播级别
*/
Propagation propagation() default Propagation.REQUIRED;

/**
* 确认执行业务方法
*/
String confirmMethod() default "";

/**
* 取消执行业务方法
*/
String cancelMethod() default "";

/**
* 事务上下文编辑
*/
Class<? extends TransactionContextEditor> transactionContextEditor() default DefaultTransactionContextEditor.class;
}

6.2 可补偿事务拦截器

先一起来看下可补偿事务拦截器对应的切面 org.mengyun.tcctransaction.interceptor.CompensableTransactionAspect,实现代码如下:
@Aspect
public abstract class CompensableTransactionAspect {

private CompensableTransactionInterceptor compensableTransactionInterceptor;

public void setCompensableTransactionInterceptor(CompensableTransactionInterceptor compensableTransactionInterceptor) {
this.compensableTransactionInterceptor = compensableTransactionInterceptor;
}

@Pointcut("@annotation(org.mengyun.tcctransaction.api.Compensable)")
public void compensableService() {
}

@Around("compensableService()")
public Object interceptCompensableMethod(ProceedingJoinPoint pjp) throws Throwable {
return compensableTransactionInterceptor.interceptCompensableMethod(pjp);
}

public abstract int getOrder();
}
CompensableTransactionInterceptor 实现代码如下
public class CompensableTransactionInterceptor {

private TransactionManager transactionManager;

private Set<Class<? extends Exception>> delayCancelExceptions;

public Object interceptCompensableMethod(ProceedingJoinPoint pjp) throws Throwable {
// 获得带 @Compensable 注解的方法
Method method = CompensableMethodUtils.getCompensableMethod(pjp);
//
Compensable compensable = method.getAnnotation(Compensable.class);
Propagation propagation = compensable.propagation();
// 获得 事务上下文
TransactionContext transactionContext = FactoryBuilder.factoryOf(compensable.transactionContextEditor()).getInstance().get(pjp.getTarget(), method, pjp.getArgs());
// 当前线程是否在事务中
boolean isTransactionActive = transactionManager.isTransactionActive();
// 判断事务上下文是否合法
if (!TransactionUtils.isLegalTransactionContext(isTransactionActive, propagation, transactionContext)) {
throw new SystemException("no active compensable transaction while propagation is mandatory for method " + method.getName());
}
// 计算方法类型
MethodType methodType = CompensableMethodUtils.calculateMethodType(propagation, isTransactionActive, transactionContext);
// 处理
switch (methodType) {
case ROOT:
return rootMethodProceed(pjp);
case PROVIDER:
return providerMethodProceed(pjp, transactionContext);
default:
return pjp.proceed();
}
}
}

6.3 资源协调者拦截器

先一起来看下资源协调者拦截器 对应的切面 org.mengyun.tcctransaction.interceptor.CompensableTransactionAspect,实现代码如下:
@Aspect
public abstract class ResourceCoordinatorAspect {

private ResourceCoordinatorInterceptor resourceCoordinatorInterceptor;

@Pointcut("@annotation(org.mengyun.tcctransaction.api.Compensable)")
public void transactionContextCall() {
}

@Around("transactionContextCall()")
public Object interceptTransactionContextMethod(ProceedingJoinPoint pjp) throws Throwable {
return resourceCoordinatorInterceptor.interceptTransactionContextMethod(pjp);
}

public void setResourceCoordinatorInterceptor(ResourceCoordinatorInterceptor resourceCoordinatorInterceptor) {
this.resourceCoordinatorInterceptor = resourceCoordinatorInterceptor;
}

public abstract int getOrder();
}
ResourceCoordinatorInterceptor 实现代码如下
public class ResourceCoordinatorInterceptor {

private TransactionManager transactionManager;

public Object interceptTransactionContextMethod(ProceedingJoinPoint pjp) throws Throwable {
Transaction transaction = transactionManager.getCurrentTransaction();
if (transaction != null) {
switch (transaction.getStatus()) {
case TRYING:
// 添加事务参与者
enlistParticipant(pjp);
break;
case CONFIRMING:
break;
case CANCELLING:
break;
}
}
// 执行方法原逻辑
return pjp.proceed(pjp.getArgs());
}
}
ResourceCoordinatorInterceptor#enlistParticipant() 实现代码如下
private void enlistParticipant(ProceedingJoinPoint pjp) throws IllegalAccessException, InstantiationException {
// 获得 @Compensable 注解
Method method = CompensableMethodUtils.getCompensableMethod(pjp);
if (method == null) {
throw new RuntimeException(String.format("join point not found method, point is : %s", pjp.getSignature().getName()));
}
Compensable compensable = method.getAnnotation(Compensable.class);
// 获得 确认执行业务方法 和 取消执行业务方法
String confirmMethodName = compensable.confirmMethod();
String cancelMethodName = compensable.cancelMethod();
// 获取 当前线程事务第一个(头部)元素
Transaction transaction = transactionManager.getCurrentTransaction();
// 创建 事务编号
TransactionXid xid = new TransactionXid(transaction.getXid().getGlobalTransactionId());
// TODO
if (FactoryBuilder.factoryOf(compensable.transactionContextEditor()).getInstance().get(pjp.getTarget(), method, pjp.getArgs()) == null) {
FactoryBuilder.factoryOf(compensable.transactionContextEditor()).getInstance().set(new TransactionContext(xid, TransactionStatus.TRYING.getId()), pjp.getTarget(), ((MethodSignature) pjp.getSignature()).getMethod(), pjp.getArgs());
}
// 获得类
Class targetClass = ReflectionUtils.getDeclaringType(pjp.getTarget().getClass(), method.getName(), method.getParameterTypes());
// 创建 确认执行方法调用上下文 和 取消执行方法调用上下文
InvocationContext confirmInvocation = new InvocationContext(targetClass,
confirmMethodName,
method.getParameterTypes(), pjp.getArgs());
InvocationContext cancelInvocation = new InvocationContext(targetClass,
cancelMethodName,
method.getParameterTypes(), pjp.getArgs());
// 创建 事务参与者
Participant participant =
new Participant(
xid,
confirmInvocation,
cancelInvocation,
compensable.transactionContextEditor());
// 添加 事务参与者 到 事务
transactionManager.enlistParticipant(participant);
}

666. 彩蛋

受限于本人的能力,蛮多处表达不够清晰或者易懂,非常抱歉。如果你对任何地方有任何疑问,欢迎添加本人微信号( wangwenbin-server ),期待与你的交流。不限于 TCC,也可以是分布式事务,也可以是微服务,以及等等。
外送一本武林秘籍:带中文注释的 TCC-Transaction 仓库地址,目前正在慢慢完善。传送门:https://github.com/YunaiV/tcc-transaction
再送一本葵花宝典:《TCC型分布式事务原理和实现》系列。
胖友,分享一个朋友圈可好?


Keytool 是一个Java数据证书的管理工具 ,Keytool将密钥(key)和证书(certificates)存在一个称为keystore的文件中在keystore里,包含两种数据:密钥实体(Key entity)-密钥(secret key)或者是私钥和配对公钥(采用非对称加密)可信任的证书实体(trusted certificate entries)-只包含公钥.
JDK中keytool常用参数说明(不同版本有差异,详细可参见【附录】中的官方文档链接):
  • -genkey 在用户主目录中创建一个默认文件”.keystore”,还会产生一个mykey的别名,mykey中包含用户的公钥、私钥和证书(在没有指定生成位置的情况下,keystore会存在用户系统默认目录)
  • -alias 产生别名 每个keystore都关联这一个独一无二的alias,这个alias通常不区分大小写
  • -keystore 指定密钥库的名称(产生的各类信息将不在.keystore文件中)
  • -keyalg 指定密钥的算法 (如 RSA DSA,默认值为:DSA)
  • -validity 指定创建的证书有效期多少天(默认 90)
  • -keysize 指定密钥长度 (默认 1024)
  • -storepass 指定密钥库的密码(获取keystore信息所需的密码)
  • -keypass 指定别名条目的密码(私钥的密码)
  • -dname 指定证书发行者信息 其中: “CN=名字与姓氏,OU=组织单位名称,O=组织名称,L=城市或区域名 称,ST=州或省份名称,C=单位的两字母国家代码”
  • -list 显示密钥库中的证书信息 keytool -list -v -keystore 指定keystore -storepass 密码
  • -v 显示密钥库中的证书详细信息
  • -export 将别名指定的证书导出到文件 keytool -export -alias 需要导出的别名 -keystore 指定keystore -file 指定导出的证书位置及证书名称 -storepass 密码
  • -file 参数指定导出到文件的文件名
  • -delete 删除密钥库中某条目 keytool -delete -alias 指定需删除的别 -keystore 指定keystore – storepass 密码
  • -printcert 查看导出的证书信息 keytool -printcert -file g:\sso\michael.crt
  • -keypasswd 修改密钥库中指定条目口令 keytool -keypasswd -alias 需修改的别名 -keypass 旧密码 -new 新密码 -storepass keystore密码 -keystore sage
  • -storepasswd 修改keystore口令 keytool -storepasswd -keystore g:\sso\michael.keystore(需修改口令的keystore) -storepass pwdold(原始密码) -new pwdnew(新密码)
  • -import 将已签名数字证书导入密钥库 keytool -import -alias 指定导入条目的别名 -keystore 指定keystore -file 需导入的证书
目录说明:
  1. 生成证书
  2. 查看证书
  3. 证书导出
  4. 附录资料
一、生成证书
 按win键+R,弹出运行窗口,输入 cmd 回车,打开命令行窗户,输入如下命令:
1
keytool -genkey -alias michaelkey -keyalg RSA -keysize 1024 -keypass michaelpwd -validity 365 -keystore g:\sso\michael.keystore -storepass michaelpwd2
截图如下:
二、查看证书
缺省情况下,-list 命令打印证书的 MD5 指纹。而如果指定了 -v 选项,将以可读格式打印证书,如果指定了 -rfc 选项,将以可打印的编码格式输出证书。
-v 命令如下:
1
keytool -list  -v -keystore g:\sso\michael.keystore -storepass michaelpwd2
回车看到的信息如下:
-rfc 命令如下:
1
keytool -list -rfc -keystore g:\sso\michael.keystore -storepass michaelpwd2
回车看到的信息如下:
三、证书的导出和查看:
导出证书命令
1
keytool -export -alias michaelkey -keystore g:\sso\michael.keystore -file g:\sso\michael.crt -storepass michaelpwd2
回车如下:
查看导出的证书信息
1
keytool -printcert -file g:\sso\michael.crt
回车看到信息如下:
四:附录
官方有关keytool命令的介绍文档:
原创文章,转载请注明: 转载自micmiu – 软件开发+生活点滴[ http://www.micmiu.com/ ]
本文链接地址: http://www.micmiu.com/lang/java/keytool-start-guide/






关于编写native方法和使用javah工具生成本地方法的头文件在这儿不讨论
这儿主要讨论如何在VS2017开发环境下搭建jni开发环境
一.java程序调用jni的环境搭建
    两种方式:
    1.直接拷贝jni开发需要使用的jni.h,jni_md.h到工程项目当中
       步骤:拷贝以下两个头文件到工程项目下面
                 jdk安装目录\include\jni.h
                 jdk安装目录\include\win32\jni_md.h
       示范:
     
    2.配置jni.h和jni_md.h文件的搜索路径
       步骤:右键点击项目》属性》C/C++》常规》附加包含目录,在其中添加路径 jdk安装目录\include和jdk安装目录\include\win32两项。
        

二.C程序通过jni反向调用jvm的环境搭建
因为C程序调用jni需要使用jvm的动态链接库,所以需要更多的配置工作。
在java程序调用jni的两种配置的基础之上,再补充三个步骤:
  1. 在windows环境变量path中添加路径"; jdk安装目录\jre\bin\client"或者"; jdk安装目录\jre\bin\server" (其中的jvm.dll文件是必须的) 
  2. 在VS(我使用的VS2013)中,右键点击项目》属性》链接器》常规》附加库目录 中添加路径: jdk安装目录\lib;
  3. 继续 右键点击项目》属性》链接器》输入》附加依赖项 中添加:jvm.lib;
          

三.注意事项
1.注意设置开发环境时,需要保证C程序或者C库的生成版本和java的版本一致。最好在项目设置当中在所有平台中进行设置,生成的时候选择对应的平台
       

四.参考

1.JNI入门教程: http://www.runoob.com/w3cnote/jni-getting-started-tutorials.html
2.VS2013中使用JNI调用java的jar包并回调到C++:http://blog.csdn.net/u013058216/article/details/53028862
3.JNI Windows学习环境搭建:http://blog.csdn.net/win2k3net/article/details/6602869

在Feed系统中,有简单数据类型的缓存,有集合类数据的。还有一些个性业务的缓存。比如大量的计数器场景,存在性判断场景等。微博解决存在性判断业务的缓存层叫EXISTENCE 缓存层,解决计算器场景的缓存叫COUNTER缓存。
EXISTENCE 缓存层主要用于缓存各种存在性判断的业务,诸如是否已赞(liked)、是否已阅读(readed)这类需求。
Feed系统内部有大量的计数场景,如用户维度有关注数、粉丝数、feed发表数,feed维度有转发数、评论数、赞数以及阅读数等。前面提到,按照传统Redis、Memcached计数缓存方案,单单存每日新增的十亿级的计数,就需要新占用百G级的内存,成本开销巨大。因此微博开发了计数服务组件CounterService。下面以计数场景来管中窥豹。
提出问题
对于计数业务,经典的构建模型有两种:1 db+cache模式,全量计数存在db,热数据通过cache加速;2全量存在Redis中。方案1 通用成熟,但对于一致性要求较高的计数服务,以及在海量数据和高并发访问场景下,支持不够友好,运维成本和硬件成本较高,微博上线初期曾使用该方案,在Redis面世后很快用新方案代替。方案2基于Redis的计数接口INCR、DECR,能很方便的实现通用的计数缓存模型,再通过hash分表,master-slave部署方式,可以实现一个中小规模的计数服务。
但在面对千亿级的历史海量计数以及每天十亿级的新增计数,直接使用Redis的计数模型存在严重的成本和性能问题。首先Redis计数作为通用的全内存计数模型,内存效率不高。存储一个key为8字节(long型id)、value为4字节的计数,Redis至少需要耗费65字节。1000亿计数需要100G*65=6.5T以上的内存,算上一个master配3个slave的开销,总共需要26T以上的内存,按单机内存96G计算,扣掉Redis其他内存管理开销、系统占用,需要300-400台机器。如果算上多机房,需要的机器数会更多。其次Redis计数模型的获取性能不高。一条微博至少需要3个计数查询,单次feed请求如果包含15条微博,仅仅微博计数就需要45个计数查询。
解决问题
在Feed系统的计数场景,单条feed的各种计数都有相同的key(即微博id),可以把这些计数存储在一起,就能节省大量的key的存储空间,让1000亿计数变成了330亿条记录;近一半的微博没有转、评论、赞,抛弃db+cache的方案,改用全量存储的方案,对于没有计数为0的微博不再存储,如果查不到就返回0,这样330亿条记录只需要存160亿条记录。然后又对存储结构做了进一步优化,三个计数和key一起一共只需要8+4*3=20字节。总共只需要16G*20=320G,算上1主3从,总共也就只需要1.28T,只需要15台左右机器即可。同时进一步通过对CounterService增加SSD扩展支持,按table滚动,老数据落在ssd,新数据、热数据在内存,1.28T的容量几乎可以用单台机器来承载(当然考虑访问性能、可用性,还是需要hash到多个缓存节点,并添加主从结构)。
计数器组件的架构如图13-14,主要特性如下:
1)  内存优化:通过预先分配的内存数组Table存储计数,并且采用 double hash 解决冲突,避免Redis 实现中的大量指针开销。
2)  Schema支持多列:一个feed id对应的多个计数可以作为一条计数记录,还支持动态增减计数列,每列的计数内存使用精简到bit;
3)  冷热数据分离,根据时间维度,近期的热数据放在内存,之前的冷数据放在磁盘,降低机器成本;
4)  LRU缓存:之前的冷数据如果被频繁访问则放到LRU缓存进行加速;
5)  异步IO线程访问冷数据:冷数据的加载不影响服务的整体性能。

图 13-14 基于Redis扩展后的计数器存储架构
通过上述的扩展,内存占用降为之前的5-10%以下,同时一条feed的评论/赞等多个计数、一个用户的粉丝/关注/微博等多个计数都可以一次性获取,读取性能大幅提升,基本彻底解决了计数业务的成本及性能问题。
欲了解更多有关分布式缓存方面的内容,请阅读《深入分布式缓存:从原理到实践》一书。

原文地址:http://blog.csdn.net/woainishifu/article/details/54017550

最近要把一个之前在32位平台下编译的项目改成64位平台,之前从来没搞过关于64位的东西,所以到处查资料,所幸搞成功了,把过程记录一下,防止以后忘记。

首先声明:64位平台无法直接调用32位dll,32平台也无法直接调用64位dll。

使用工具:VS2010

首先,用VS2010打开之前的项目,这时候项目的平台是32位的,如下图所示:

点击Win32那里,选择“配置管理器”:

还是点击“Win32”那里,选择“新建”:
可以看到其实x64平台已经存在了,选择x64,然后下面“从此处复制设置”就选择“Win32”

点击“确定”就可以了。然后查看一下“属性 -> 链接器 -> 高级 -> 目标计算机”是否是x64的:

这样就完成了!编译运行即可生成64位的dll,只不过这次生成的dll不是在系统目录的Debug或者Release文件夹下,而是在一个x64文件夹下对应的Debug和Release文件夹下面。


原文地址:https://www.jianshu.com/p/fd18fa1d09d2?utm_campaign=haruki&utm_content=note&utm_medium=reader_share&utm_source=qq
在经过了几次跳票之后,Java 9终于在原计划日期的整整一年之后发布了正式版。Java 9引入了很多新的特性,除了闪瞎眼的Module SystemREPL,最重要的变化我认为是默认GC(Garbage Collector)修改为新一代更复杂、更全面、性能更好的G1(Garbage-First)。JDK的维护者在GC选择上一直是比较保守的,G1从JDK 1.6时代就开始进入开发者的视野,直到今天正式成为Hotspot的默认GC,也是走了很长的路。
本文将主要讲解GC调优需要知道的一些基础知识,会涉及到一些GC的实现细节,但不会对实现细节做很全面的阐述,如果你看完本文之后,能对GC有一个大致的认识,那本文的写作目的也就达到了。由于在这次写作过程中,恰逢Java 9正式版发布,之前都是依赖Java 8的文档写的,如果有不正确的地方还望指正。本文将包含以下内容:
  1. GC的作用范围
  2. GC负责的事情
  3. JVM中的4种GC
  4. G1的一些细节
  5. 使用Java 9正式版对G1进行测试
  6. 一些简单的GC调优方法

一、GC的作用范围

要谈GC的作用范围,首先要谈JVM的内存结构,JVM内存中主要有以下几个区域:堆、方法区(JVM规范中的叫法,Hotspot大致对应的是Metaspace)、栈、本地方法栈、PC等,其中GC主要作用在堆上,如下图所示:
JVM内存结构
其中堆和方法区是所有线程共享的,其他则为线程独有,HotSpot JVM使用基于分代的垃圾回收机制,所以在堆上又分为几个不同的区域(在G1中,各年龄代不再是连续的一整片内存,为了描述方便,这里还使用传统的表示方法),具体如下图所示:
JVM堆中的分区

二、GC负责的事情

GC的发展是随着JDK(Standard Edition)的发展一步步发展起来的,垃圾回收(Garbage Collection)可以说是JDK里最影响性能的行为了。GC做的事情,说白了就是「通过对内存进行管理,以保障在内存足够的时候,程序可以正常的使用内存」。具体而言,GC通常做的事情有以下3个:

1. 分配对象和对象的年龄管理

通常而言,GC需要管理「在上图中的年轻代(Young)分配对象,然后通过一系列的年龄管理,将之销毁或晋升到老年代(Tenured)中去」的过程。这个过程会伴随着若干次的Minor GC。
对于普通的对象而言,分配内存是一件很简单而且快速的事情。在对象还未创建时,其所占内存大小通过类的元数据就可以确定,而Eden区域的内存可以认为是连续的,所以给对象分配内存要做的只是在上图中Eden区域中把指针移动相应的长度,并将地址返回给对象的引用即可。当然实际的过程比这个复杂,在下文中会提到。
不过,有时候一个对象会直接在老年代中创建,这个点也会在后边提到。

2. 在老年代中进行标记

老年代的GC算法可以大致是认为是一个标记-整理(Mark-Compact,其实是混合了标记-清理,标记-复制和标记-整理)算法,所以老年代的垃圾清理首先要做的就是在老年代对存活的对象(可达性分析,关于不同的可达性可以参考JDK解构 - Java中的引用和动态代理的实现)进行标记,对于寻求大吞吐量的服务器应用来说,这个过程往往需要是并发的。
标记的过程发生在Major GC被触发之后,不同的GC对于MajorGC的触发条件和标记过程的实现也不尽相同。

3. 在老年代中进行压缩

在上一条的基础上,将还存活的对象进行压缩(CMS和G1的行为与此有些不同之处),压缩的过程就是将存活的对象从老年代的起点进行挨个复制,使得老年代维持在一片连续的内存中,消除内存碎片,对于内存分配速度的提升会有很大的帮助。

三、GC的种类

Hotspot会根据宿主机的硬件特性和操作系统类型,将之分为客户端型(client-class)或者服务器型(server-class),如果是服务器型主机,Java 9之前默认使用Parallel GC,Java 9中默认使用G1。对于服务器型主机的选择标准是「CPU核心数大于1,内存大于2GB」,所以现在大部分的主机都可以认为是服务器型主机。
这里讨论的所有GC都是基于分代垃圾回收算法的。

1. Serail

Serail是最早的一款GC,它只使用一个线程来做所有的Minor和Major垃圾回收。它在运行时,其他所有的事情都会暂停。其工作方式十分简单,在需要GC的安全点,它会停止所有其他线程(Stop-The-World),对年轻代进行标记-复制,或对老年代进行标记-整理。
可以使用JVM参数-XX:+UseSerialGC来开启此GC,当使用此参数时,年轻代和老年代将都是用Serial来做垃圾回收。在年轻代使用标记-复制算法,将Eden中存活的对象和非空的Suvivor区(From)中存活的对象复制到空的Suvivor区(To)中去,同时将一部分Suvivor中的对象晋升到老年代去。在老年代则使用标记-整理算法。
看起来Serial古老而简陋,但在宿主机资源紧张或者JVM堆很小的情况下(比如堆内存大小只有不到100M),Serial反而可以达到更好的效果,因为其他并发或并行GC都是基于多线程的,会带来额外的线程切换和线程间通信的开销。

2. Parallel/Throughput

Parallel在Java 9之前是服务器型宿主机中JVM的默认GC,其垃圾回收的算法和Serial基本相同,不同之处在与它使用多线程来执行。由于使用了多线程,可以享受多核CPU带来的优势,可以通过参数-XX:+UseParallelGC -XX:+UseParallelOldGC显示指定。

3. CMS

CMS和G1都属于「Mostly Concurrent Mark and Sweep Garbage Collector」,可以使用-XX:+UseConcMarkSweepGC参数打开。CMS的年轻代垃圾回收使用的是Parallel New来做,其行为和Parallel中的差不多相同,他们的实现上有一些不同的地方,比如Parallel可以自动调节年轻代中各区的大小,用的是广度优先搜索等。
老年代使用CMS,CMS的回收和Parallel也基本类似,不同点在与,CMS使用的更复杂的可达性分析步骤,并且不是每次都做压缩的动作,这样达到的效果就是,Stop-The-World的时长会降低,JVM运行中断的时间减少,适合在对延迟敏感的场景下使用。
CMS在Java 9中已经被废弃,但了解CMS的行为对理解G1会有一些帮助,所以这里还是会简单的叙述一下。CMS的步骤大致如下:
  1. 第一次标记
    从GC Roots开始,找到它们在老年代中第一个可达的对象,这些对象或者是直接被GC Roots引用,或者通过年轻代中的对象被GC Roots引用。这一步会Stop-The-World。
  2. 并发标记
    在第一次标记的基础上,进一步进行可达性分析,从而标记存活的对象。这一步叫「并发」标记,是因为做标记的线程是和应用的工作线程并发执行的,也就是说,这一步不会Stop-The-World。
  3. 第二次标记
    在并发标记的过程中,由于程序仍在执行,会导致在并发标记完成后,有一些对象的可达性会发生变化,所以需要再次对他们进行标记。这一步会Stop-The-World。
  4. 清理
    回收不使用的对象,留作以后使用。
CMS的设计比较复杂,所以也带来了一些问题,比如浮动垃圾(Floating Garbage,指的是在第一步标记可达,但在第二步执行的同时已经不可达的对象),由于不做老年代压缩,导致老年代会出现较多的内存碎片。

4. G1

由于「引入了并发标记」和「不做老年代压缩」,CMS可以带来更好的响应时延表现,但同时也带来了一些问题。G1本身就是作为CMS的替代品出现的,在它的使用场景里,堆不再是连续的被分为上文所说的各种代,整个堆会被分为一个个区域(Region),每个区域可以是任何代。如下图所示:
使用G1的JVM某时刻的堆内存
其中有红色方框的为年轻代(标S的为Survivor区域,其他为Eden),其他蓝色底的区域为老年代(标H的为大对象区域,用以存储大对象)。

四、G1的一些细节

G1与以上3种GC相同,也是基于分代的垃圾回收器。它的垃圾回收步骤可以分为年轻代回收(Young-only phase,类似于Minor GC)和混合垃圾回收阶段(Space-reclamation phase)。下图是Oracle文档中对于此两个阶段的示意图:
jsgct_dt_001_grbgcltncyl.png
G1设计目标和适用对象
G1的设计目标是让大型的JVM可以动态的控制GC的行为以满足用户配置的性能目标。G1会在平衡吞吐和响应时延的基础上,尽可能的满足用户的需求。它适用的JVM往往有以下特征:
  1. 堆的大小可能达到数十G(或者更大),同时存活的对象数量也很多。
  2. 对象的分配和年龄增长的行为随着程序的运行不断的变化
  3. 堆上很容易形成碎片
  4. 要求较少的Stop-The-World暂停时间,通常小于数百毫秒
对G1的行为进行测试
如果想要看垃圾回收的具体执行过程,可以使用虚拟机参数-Xlog:gc*=debug或者-Xlog:gc*=info,前一个会打印更多的细节。注意传统的VM参数-XX:+PrintGCDetails在Java9中已经废弃,会有Warning信息。可以使用以下代码中的程序去测试:
static int TOTAL_SIZE = 1024 * 5;
static Object[] floatingObjs= new Object[TOTAL_SIZE];
static LinkedList<Object> immortalObjs = new LinkedList<Object>();
//释放浮动垃圾synchronized static void renewFloatingObjs() {
System.err.println("存活对象满========================================");
if (floatingSize + 5 >= TOTAL_SIZE) {
floatingObjs= new Object[TOTAL_SIZE];
floatingSize = 0;
}
}
//添加浮动垃圾synchronized static void addObjToFloating(Object obj) {
if (floatingSize++ < TOTAL_SIZE) {
floatingObjs[floatingSize] = obj;
if (immortalSize++ < TOTAL_SIZE) {
immortalObjs.add(obj);
} else {
immortalObjs.remove(new Random().nextInt(TOTAL_SIZE));
immortalObjs.add(obj);
}
}
}

public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
while (true) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Byte[] garbage = new Byte[1024 * (1 + new Random().nextInt(20))];
if (new Random().nextInt(20) < 2) {
if (floatingSize + 5 >= TOTAL_SIZE) {
renewFloatingObjs();
}
addObjToFloating(garbage);
}
}
}).start();
}
}
在这段代码中,模拟了常规程序的使用情况。不断的生成新的大小不等的对象,这些对象中会有大约10%的机会进入浮动垃圾floatingObjs,浮动垃圾会被定期清除。同时会有一部分的对象进入immortalObjs,这些对象被释放的机会更少,它们大概率将成为老年代的常住用户。
从上边的测试可以得到如下GC日志1,这是一次完整的年轻代GC,从中可以看到,默认的区域大小为1M,同时将开始一次Full GC,其格式大致为[<虚拟机运行的时长>][<日志级别>][<标签>] GC(<GC的标识>) <其他信息>
//日志1
[0.014s][info][gc,heap] Heap region size: 1M
//一次完整的年轻代垃圾回收,伴随着一次暂停
[12.059s][info ][gc,start ] GC(18) Pause Young (G1 Evacuation Pause)
[12.059s][info ][gc,task ] GC(18) Using 8 workers of 8 for evacuation
[12.078s][info ][gc,phases ] GC(18) Pre Evacuate Collection Set: 0.0ms
[12.078s][info ][gc,phases ] GC(18) Evacuate Collection Set: 18.6ms
[12.079s][info ][gc,phases ] GC(18) Post Evacuate Collection Set: 0.3ms
[12.079s][info ][gc,phases ] GC(18) Other: 0.3ms
[12.079s][info ][gc,heap ] GC(18) Eden regions: 342->0(315)
[12.079s][info ][gc,heap ] GC(18) Survivor regions: 38->35(48)
[12.079s][info ][gc,heap ] GC(18) Old regions: 425->463
[12.079s][info ][gc,heap ] GC(18) Humongous regions: 0->0
[12.078s][debug][gc,ergo,ihop ] GC(18) Request concurrent cycle initiation (occupancy higher than threshold) occupancy: 485490688B allocation request: 0B threshold: 472331059B (45.00) source: end of GC
[12.078s][debug][gc,ihop ] GC(18) Basic information (value update), threshold: 472331059B (45.00), target occupancy: 1049624576B, current occupancy: 521069456B, recent allocation size: 20640B, recent allocation duration: 817.38ms, recent old gen allocation rate: 25251.50B/s, recent marking phase length: 0.00ms
[12.078s][debug][gc,ihop ] GC(18) Adaptive IHOP information (value update), threshold: 472331059B (47.37), internal target occupancy: 997143347B, occupancy: 521069456B, additional buffer size: 367001600B, predicted old gen allocation rate: 318128.08B/s, predicted marking phase length: 0.00ms, prediction active: false
[12.078s][debug][gc,ergo,refine ] GC(18) Updated Refinement Zones: green: 15, yellow: 45, red: 75
[12.079s][info ][gc,heap ] GC(18) Eden regions: 342->0(315)
[12.079s][info ][gc,heap ] GC(18) Survivor regions: 38->35(48)
[12.079s][info ][gc,heap ] GC(18) Old regions: 425->463
[12.079s][info ][gc,heap ] GC(18) Humongous regions: 0->0
[12.079s][info ][gc,metaspace ] GC(18) Metaspace: 5172K->5172K(1056768K)
[12.079s][debug][gc,heap ] GC(18) Heap after GC invocations=19 (full 0):
[12.079s][info ][gc ] GC(18) Pause Young (G1 Evacuation Pause) 803M->496M(1001M) 19.391ms
[12.079s][info ][gc,cpu ] GC(18) User=0.05s Sys=0.00s Real=0.02s
年轻代回收(Young-only)
对于纯粹的年轻代回收,其算法很简单,与Parallel和CMS的年轻代十分类似,这是一个多线程并行执行的过程,同样需要Stop-The-World(对应上边日志中的Pause Young),停下来所有的工作线程,然后将Eden上存活的对象拷贝到Suvivor区域,这里会将很多个对象从多个不同的区域拷贝到少数的几个区域内,所以这一步在G1中叫做疏散(Evacuation),同时把Suvivor上触及年龄阈值的对象晋升到老年代区域。
老年代回收(concurrent cycle)
G1的老年代回收是在老年代空间触及一个阈值(Initiating Heap Occupancy Percent)之后,这个回收伴随着年轻代的回收工作,但与上边所说的回收有些不同。
  1. 年轻代回收:伴随着年轻代的回收工作,同时会执行并发标记和一部分清理的工作,这样可以共用年轻代垃圾回收的Stop-The-World。
    1. 第一次标记:对应一次Pause Initial Mark
      和CMS的步骤类似,首先进行第一次标记。但实现方法上有很大的区别,G1首先对当前堆上的对象情况进行一个虚拟快照(Snapshot-At-The-Beginning),然后根据这个快照对老年代的对象和区域进行标记,并执行之后的垃圾回收。之后像CMS一样会有并发标记的过程。
      这样会产生一个问题,在这次回收结束之后,会有些对象在并发标记的过程中,它的可达性已经变化,导致已经不可达的对象仍然没有被回收。但是这样能带来更好的响应时间。
    2. 重新标记:对应一次Pause Remark
      在这个阶段,G1首先完成上一步开始的标记工作,之后会对特殊引用的对象进行处理(具体可以参考JDK解构 - Java中的引用和动态代理的实现),还有对Metaspace区域进行垃圾回收。这一步会进行Stop-The-World。
    3. 清理:对应一次Pause Cleanup
      这一步主要做的是收集当前堆中的内存区域信息,对空的区域进行回收,为接下来的空间回收做一些准备工作,清理结束之后,通常会伴随着一次年轻代回收,如果判断不需要进行空间回收,则会进入下一个年轻代回收的工作。这一步会进行Stop-The-World。
  2. 混合垃圾回收:对应一次或多次Pause Mixed
    主要做的是对老年代的区域内存进行疏散(Evacuation),也包含对年轻代的区域回收工作。同时这一步也会动态地调整IHOP
从对G1的GC日志的分析,可以看到G1的垃圾回收行为是基于一个可预测的模型:GC会不断的主动触发垃圾回收,在这个过程中不断地进行信息统计和系统GC参数的设置,然后将上边这些步骤安排在这些垃圾回收过程中。

大对象的分配

正常情况下,一个对象会在年轻代的Eden中创建,然后通过垃圾回收和年龄管理之后,晋升到老年代。但对于某些比较大的对象,可能会直接分配到老年代去。
对于G1,对象大多数情况都会在Eden上分配,如果JVM判断一个对象为大对象(其阈值可以通过-XX:G1HeapRegionSize来设置),则会直接分配如老年代的大对象区域中。
对于其他的内存区域连续的GC,下面是从StackOverflow上搬运过来的对象在堆上的分配过程:
  1. 使用 thread local allocation buffer (TLAB), 如果空间足够,则分配成功。
    从名称便可知,TLAB是线程独占的,所以线程安全,且速度非常快。如果一个TLAB满了,线程会被分配一个新的TLAB。
  2. 如果TLAB 空间不够这次分配对象,但其中还有很多空间可用,则不使用TLAB,直接在Eden中分配对象。
    直接在Eden上分配对象要去抢占Eden中的指针操作,其代价较使用TLAB要大一些。
  3. 如果Eden的对象分配失败,出发Minor GC。
  4. 如果Minor GC完成后还不够,则直接分配到老年代。

一些简单的GC调优方法

1. 使用不同的索引对象
引用的类型会直接影响其所引用对象的GC行为,当要做一些内存敏感的应用时,可以参考使用合适的引用类型。具体可以参考JDK解构 - Java中的引用和动态代理的实现
2. 使用Parallel
从上文中可知,Java 8默认的GC是Parallel,它也叫Throughput,所以它的目的是尽可能的增加系统的吞吐量。在Parallel里,可以通过参数调节最大停止时间(-XX:MaxGCPauseMillis,默认无设置)和吞吐量(-XX:GCTimeRatio,默认值是99,即最大使用1%的时间来做垃圾回收)来调优GC的行为。其中设置最大停止时间可能会导致GC调节各年龄代分区的尺寸(通过增量来实现)。
3. 使用G1
从Java 9开始G1变成了默认的GC,G1中有一些细节的概念在上文中没有叙述,这里先介绍一下:
  1. Remembered Sets(Rsets):对于每个区域,都有一个集合记录这个区域中所有的引用。
  2. G1 refinement:G1中需要有一系列的线程不断地维护Rsets。
  3. Collection Sets(Csets):在垃圾回收中需要被回收的区域,这些区域中的可达对象(活着的对象)会被疏散。这些区域可能是任何年龄代。
  4. 写屏障(Write Barriers):对于每一次赋值操作,G1都会有两个写屏障,写之前(Pre-Write)一个,写之后(Post-Write)一个。Pre-write主要与SATB相关,Post-write主要与Rsets相关
  5. Dirty Card Queue:写屏障会将写的记录放入这个队列,会有线程将这里的对象不断的刷入Rsets。
  6. Green/Yellow/Red Zone:三个会影响处理Dirty Card Queue线程数的阈值。根据Dirty Card Queue中元素的个数,可以来设置一些GC行为(可以认为是逻辑上将Dirty Card Queue分隔成多个区域)。Green表示超过此阈值则开始新建线程来处理这个队列,Yellow表示超过此阈值,强制启动这些线程,Red表示超过此阈值则会让写操作的线程自己来执行G1 refinement。
G1提供了丰富的基于不同目的的可调优的参数,列表如下:
参数
描述
-XX:+G1UseAdaptiveConcRefinement,
调节G1 refinement所使用的资源。
-XX:G1ConcRefinementGreenZone=<ergo>,
调节G1 refinement所使用的资源。
-XX:G1ConcRefinementYellowZone=<ergo>,
调节G1 refinement所使用的资源。
-XX:G1ConcRefinementRedZone=<ergo>,
调节G1 refinement所使用的资源。
-XX:G1ConcRefinementThreads=<ergo>
调节G1 refinement所使用的资源。
-XX:G1RSetUpdatingPauseTimePercent=10
调节G1 refinement所需要的时间在整个垃圾回收时间的比例,G1会根据这个时间动态地调节第一行的各个参数。
-XX:+ReduceInitialCardMarks
批量执行对象的生成,以减少初始标记的时间
-XX:-ParallelRefProcEnabled
使用多线程处理上文中所说的在重新标记阶段对引用的处理
-XX:G1SummarizeRSetStatsPeriod=<n>
设置n次垃圾回收后,打印Rsets的总结性报告。
-XX:GCTimeRatio=<n>
设置GC吞吐量。GC总共应该使用的时间是1 / (1 + n),这个参数会影响不同年龄代尺寸的增长。
-XX:G1HeapRegionSize
设置区域的大小
主要参考文档:
  1. Getting Started with the G1 Garbage Collector
  2. Garbage-First Garbage Collector Tuning
  3. Evaluating and improving remembered sets in the HotSpot G1 garbage collector
  4. G1GC Internals
  5. GC Algorithms: Basics
  6. Java中几种常量池的区分、、
小礼物走一走


作者:TheAlchemist
链接:https://www.jianshu.com/p/fd18fa1d09d2
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

TLAB技术是为了解决堆内存分配中的多线程并发问题(无论是指针碰撞或者空闲列表方案都有并发问题)
解决该问题的方法有两个:
1.CAS自旋(针对指针碰撞方案的效率特别高)
2.TLAB技术
TLAB技术就是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在java堆中预先分配一小块内存,这块内存就称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。
虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数决定

TLAB技术只是一种预先给线程分配所属分配区域的方法,在这个区域内的内存分配方案还是会使用指针碰撞或者空闲列表

待写

原文地址:http://www.cnblogs.com/xd502djj/archive/2010/09/21/1832493.html
__declspec(dllexport)
声明一个导出函数,是说这个函数要从本DLL导出。我要给别人用。一般用于dll中 
省掉在DEF文件中手工定义导出哪些函数的一个方法。当然,如果你的DLL里全是C++的类的话,你无法在DEF里指定导出的函数,只能用__declspec(dllexport)导出类
__declspec(dllimport)
声明一个导入函数,是说这个函数是从别的DLL导入。我要用。一般用于使用某个dll的exe中 
不使用 __declspec(dllimport) 也能正确编译代码,但使用 __declspec(dllimport) 使编译器可以生成更好的代码。编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于 DLL 中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨 DLL 边界的函数调用中。但是,必须使用 __declspec(dllimport) 才能导入 DLL 中使用的变量。
  
使用举例: 
  // File: SimpleDLLClass.h
#ifdef SIMPLEDLL_EXPORT
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT 
#endif
class DLL_EXPORT SimpleDLLClass
{
public:
 SimpleDLLClass();
 virtual ~SimpleDLLClass();
 virtual int getValue() { return m_nValue;};
private:
 int m_nValue;
};
 
 
// File: SimpleDLLClass.cpp
#include "SimpleDLLClass.h"
SimpleDLLClass::SimpleDLLClass()
{
 m_nValue=0;
}
SimpleDLLClass::~SimpleDLLClass()
{
}
 
说明:
1. 在你的APP中include SimpleDLLClass.h时,如果你的APP的项目不定义SIMPLEDLL_EXPORT,则DLL_EXPORT不存在。此时APP仍可以正常运行。
 
 
// File: SimpleDLLClass.h
static int m_nValue;
// File: SimpleDLLClass.cpp
int SimpleDLLClass::m_nValue=0;
 
说明:
1. 如果你的APP的项目不定义SIMPLEDLL_EXPORT,则DLL_EXPORT不存在。此时APP无法LINK。原因是找不到m_nValue。(原因:静态变量m_nValue已被DLL导出,但SimpleDLLClass无法访问m_nValue)
Workaround:
#define DLL_EXPORT __declspec(dllimport)
Conclusion:
dllimport是为了更好的处理类中的静态成员变量(或者其他...)的,如果没有静态成员变量(或者其他...),那么这个__declspec(dllimport)无所谓.
/////////////////////////
在Windows DLL编程时,可使用__declspec(dllimport)关键字导入函数或者变量。
函数的导入
    当你需要使用DLL中的函数时,往往不需要显示地导入函数,编译器可自动完成。但如果你显示地导入函数,编译器会产生质量更好的代码。由于编译器确切地知道了一个函数是否在一个DLL中,它就可以产生更好的代码,不再需要间接的调用转接。
    Win32的PE格式(Portable Executable Format)把所有导入地址放在一个导入地址表中。下面用一个具体实例说明使用__declspec(dllimport)导入函数和不使用的区别:
    假设func是一个DLL中的函数,现在在要生成的.exe的main函数中调用func函数,并且不显示地导入func函数(即没有:__declspec(dllimport)),代码示例如下:
    int main()
    {
        func();
    }
编译器将产生类似这样的调用代码:
    call func
然后,链接器把该调用翻译为类似这样的代码:
    call 0x40000001       ; ox40000001是"func"的地址
并且,链接器将产生一个Thunk,形如:
    0x40000001: jmp DWORD PTR __imp_func
这里的imp_func是func函数在.exe的导入地址表中的函数槽的地址。然后,加载器只需要在加载时更新.exe的导入地址表即可。
    而如果使用了__declspec(dllimport)显示地导入函数,那么链接器就不会产生Thunk(如果不被要求的话),而直接产生一个间接调用。因此,下面的代码:
    __declspec(dllimport) void func1(void);
   void main(void) 
    {
        func1();
    }
将调用如下调用指令:
    call DWORD PTR __imp_func1
    因此,显示地导入函数能有效减少目标代码(因为不产生Thunk)。另外,在DLL中使用DLL外的函数也可以这样做,从而提高空间和时间效率。
变量的导入
    与函数不同的是,在使用DLL中的变量时,需要显示地导入变量。使用__declspec(dllimport)关键字导入变量。若在DLL中使用.def导出变量,则应使用DATA修饰变量,而不是使用已经被遗弃的CONSTANT。因为CONSTANT可能需要使用指针间接访问变量,不确定什么时候会出问题。
//////////////
我相信写WIN32程序的人,做过DLL,都会很清楚__declspec(dllexport)的作用,它就是为了省掉在DEF文件中手工定义导出哪些 函数的一个方法。当然,如果你的DLL里全是C++的类的话,你无法在DEF里指定导出的函数,只能用__declspec(dllexport)导出 类。但是,MSDN文档里面,对于__declspec(dllimport)的说明让人感觉有点奇怪,先来看看MSDN里面是怎么说的: 
不使用 __declspec(dllimport) 也能正确编译代码,但使用 __declspec(dllimport) 使编译器可以生成更好的代码。编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于 DLL 中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨 DLL 边界的函数调用中。但是,必须使用 __declspec(dllimport) 才能导入 DLL 中使用的变量。
初看起来,这段话前面的意思是,不用它也可以正常使用DLL的导出库,但最后一句话又说,必须使用 __declspec(dllimport) 才能导入 DLL 中使用的变量这个是什么意思??
那我就来试验一下,假定,你在DLL里只导出一个简单的类,注意,我假定你已经在项目属性中定义了 SIMPLEDLL_EXPORT
SimpleDLLClass.h
#ifdef SIMPLEDLL_EXPORT#define DLL_EXPORT __declspec(dllexport)#else#define DLL_EXPORT#endifclass DLL_EXPORT SimpleDLLClass{public: SimpleDLLClass(); virtual ~SimpleDLLClass(); virtual int getValue() { return m_nValue;};private: int m_nValue;};SimpleDLLClass.cpp
#include "SimpleDLLClass.h"SimpleDLLClass::SimpleDLLClass(){ m_nValue=0;}SimpleDLLClass::~SimpleDLLClass(){}然后你再使用这个DLL类,在你的APP中include SimpleDLLClass.h时,你的APP的项目不用定义 SIMPLEDLL_EXPORT 所以,DLL_EXPORT 就不会存在了,这个时候,你在APP中,不会遇到问题。这正好对应MSDN上说的__declspec(dllimport)定义与否都可以正常使用。但 我们也没有遇到变量不能正常使用呀。 那好,我们改一下SimpleDLLClass,把它的m_nValue改成static,然后在cpp文件中加一行
int SimpleDLLClass::m_nValue=0;如果你不知道为什么要加这一行,那就回去看看C++的基础。 改完之后,再去LINK一下,你的APP,看结果如何, 结果是LINK告诉你找不到这个m_nValue。明明已经定义了,为什么又没有了?? 肯定是因为我把m_nValue定义为static的原因。但如果我一定要使用Singleton的Design Pattern的话,那这个类肯定是要有一个静态成员,每次LINK都没有,那不是完了? 如果你有Platform SDK,用里面的Depend程序看一下,DLL中又的确是有这个m_nValue导出的呀。
再回去看看我引用MSDN的那段话的最后一句。 那我们再改一下SimpleDLLClass.h,把那段改成下面的样子:
#ifdef SIMPLEDLL_EXPORT#define DLL_EXPORT __declspec(dllexport)#else#define DLL_EXPORT __declspec(dllimport)#endif再LINK,一切正常。原来dllimport是为了更好的处理类中的静态成员变量的,如果没有静态成员变量,那么这个__declspec(dllimport)无所谓。


原文地址:http://blog.csdn.net/jia_xiaoxin/article/details/2868216
   关于函数的调用规则(调用约定),大多数时候是不需要了解的,但是如果需要跨语言的编程,比如VC写的dll要delphi调用,则需要了解。
        microsoft的vc默认的是__cdecl方式,而windows API则是__stdcall,如果用vc开发dll给其他语言用,则应该指定__stdcall方式。堆栈由谁清除这个很重要,如果是要写汇编函数给C调用,一定要小心堆栈的清除工作,如果是__cdecl方式的函数,则函数本身(如果不用汇编写)则不需要关心保存参数的堆栈的清除,但是如果是__stdcall的规则,一定要在函数退出(ret)前恢复堆栈。
1.__cdecl
       所谓的C调用规则。按从右至左的顺序压参数入栈,由调用者把参数弹出栈。切记:对于传送参数的内存栈是由调用者来维护的。返回值在EAX中因此,对于象printf这样变参数的函数必须用这种规则。编译器在编译的时候对这种调用规则的函数生成修饰名的饿时候,仅在输出函数名前加上一个下划线前缀,格式为_functionname。 
2.__stdcall 
        按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。_stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,切记:函数自己在退出时清空堆栈返回值在EAX中。  __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。如函数int func(int a, double b)的修饰名是_func@12
3.__fastcall
       __fastcall调用的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。这个和__stdcall很象,唯一差别就是头两个参数通过寄存器传送。注意通过寄存器传送的两个参数是从左向右的,即第一个参数进ECX,第2个进EDX,其他参数是从右向左的入stack。返回仍然通过EAX.
4.__pascal
       这种规则从左向右传递参数,通过EAX返回,堆栈由被调用者清除
 
5.__thiscall
        仅仅应用于"C++"成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定
 
       调用约定可以通过工程设置:Setting.../C/C++ /Code Generation项进行选择,缺省状态为__cdecl。
 
名字修饰约定:
1、修饰名(Decoration name):"C"或者"C++"函数在内部(编译和链接)通过修饰名识别
2、C编译时函数名修饰约定规则:
__stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个"@"符号和其参数的字节数,格式为_functionname@number,例如 :function(int a, int b),其修饰名为:_function@8
__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
__fastcall调用约定在输出函数名前加上一个"@"符号,后面也是一个"@"符号和其参数的字节数,格式为@functionname@number。
3、C++编译时函数名修饰约定规则:
__stdcall调用约定:
1)、以"?"标识函数名的开始,后跟函数名;
2)、函数名后面以"@@YG"标识参数表的开始,后跟参数表;
3)、参数表以代号表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复;
4)、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前; 
5)、参数表后以"@Z"标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。
其格式为"?functionname@@YG*****@Z"或"?functionname@@YG*XZ",例如
          int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
          void Test2()                       -----“?Test2@@YGXXZ
 
__cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YA"。
__fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YI"。
VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用.

注意:
1、_beginthread需要__cdecl的线程函数地址,_beginthreadex和CreateThread需要__stdcall的线程函数地址。
2、一般WIN32的函数都是__stdcall。而且在Windef.h中有如下的定义:
#define CALLBACK __stdcall
#define WINAPI  __stdcall
3、extern "C" _declspec(dllexport) int __cdecl Add(int a, int b);
   typedef int (__cdecl*FunPointer)(int a, int b);
   修饰符的书写顺序如上。
4、extern "C"的作用:如果Add(int a, int b)是在c语言编译器编译,而在c++文件使用,则需要在c++文件中声明:extern "C" Add(int a, int b),因为c编译器和c++编译器对函数名的解释不一样(c++编译器解释函数名的时候要考虑函数参数,这样是了方便函数重载,而在c语言中不存在函数重载的问题),使用extern "C",实质就是告诉c++编译器,该函数是c库里面的函数。如果不使用extern "C"则会出现链接错误。
一般象如下使用:
#ifdef _cplusplus 
#define ETERN_C extern "C"
#else
#define EXTERN_C extern
#endif
#ifdef _cplusplus 
extern "C"{
#endif 
EXTERN_C int func(int a, int b); 
#ifdef _cplusplus 
} 
#endif
5、MFC提供了一些宏,可以使用AFX_EXT_CLASS来代替__declspec(DLLexport),并修饰类名,从而导出类,AFX_API_EXPORT来修饰函数,AFX_DATA_EXPORT来修饰变量
AFX_CLASS_IMPORT:__declspec(DLLexport)
AFX_API_IMPORT:__declspec(DLLexport)
AFX_DATA_IMPORT:__declspec(DLLexport)
AFX_CLASS_EXPORT:__declspec(DLLexport)
AFX_API_EXPORT:__declspec(DLLexport)
AFX_DATA_EXPORT:__declspec(DLLexport)
AFX_EXT_CLASS:#ifdef _AFXEXT 
    AFX_CLASS_EXPORT
        #else
    AFX_CLASS_IMPORT
6、DLLMain负责初始化(Initialization)和结束(Termination)工作,每当一个新的进程或者该进程的新的线程访问DLL时,或者访问DLL的每一个进程或者线程不再使用DLL或者结束时,都会调用DLLMain。但是,使用TerminateProcess或TerminateThread结束进程或者线程,不会调用DLLMain。
7、一个DLL在内存中只有一个实例
DLL程序和调用其输出函数的程序的关系:
1)、DLL与进程、线程之间的关系
DLL模块被映射到调用它的进程的虚拟地址空间。
DLL使用的内存从调用进程的虚拟地址空间分配,只能被该进程的线程所访问。
DLL的句柄可以被调用进程使用;调用进程的句柄可以被DLL使用。
DLLDLL可以有自己的数据段,但没有自己的堆栈,使用调用进程的栈,与调用它的应用程序相同的堆栈模式。
2)、关于共享数据段
DLL定义的全局变量可以被调用进程访问;DLL可以访问调用进程的全局数据。使用同一DLL的每一个进程都有自己的DLL全局变量实例。如果多个线程并发访问同一变量,则需要使用同步机制;对一个DLL的变量,如果希望每个使用DLL的线程都有自己的值,则应该使用线程局部存储(TLS,Thread Local Strorage)。


原文地址:http://mp.weixin.qq.com/s?__biz=MzU0OTE4MzYzMw    ==&mid=2247484464&idx=1&sn=5669d142ee93d15c351ebaa6e57d0cd6&chksm=fbb28dceccc504d8ae1ce1b31dd5760c48aa8e3b32f86b98aa482d2bf22d4dc53be70f1d1983&scene=0#rd

简介

  我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标非常关键。举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Web服务器,配置MaxClients为500个(表示服务器的最大连接数目)。
那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):
20*500/0.1 = 100000 (10万QPS)
在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增加。
就Web服务器而言,他打开了越多的连接进程,CPU需要处理的上下文切换也越多,额外增加了CPU的消耗,然后就直接导致平均响应时间增加。因此上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。可以通过Apache自带的ab来测试一下,取一个合适的值。然后,我们选择内存操作级别的存储的Redis,在高并发的状态下,存储的响应时间至关重要。网络带宽虽然也是一个因素,不过,这种请求数据包一般比较小,一般很少成为请求的瓶颈。负载均衡成为系统瓶颈的情况比较少,在这里不做讨论哈。
那么问题来了,假设我们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际情况,甚至更多):
20*500/0.25 = 40000 (4万QPS)
于是,我们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。
举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定出现大塞车。(5条车道忽然变成4条车道的感觉)
同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。
 
其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数占满,其他正常的业务请求,无连接进程可用。
更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。

重启与过载保护

如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。
秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回

高并发下的数据安全

我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

超发的原因

假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。
 
在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

优化方案1

将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false

<?php//优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回falseinclude('./mysql.php');
$username = 'wang'.rand(0,1000);//生成唯一订单function build_order_no(){
return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}//记录日志function insertLog($event,$type=0,$username){
global $conn;
$sql="insert into ih_log(event,type,usernma)
values('$event','$type','$username')";
return mysqli_query($conn,$sql);
}function insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number){
global $conn;
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price,username,number)
values('$order_sn','$user_id','$goods_id','$sku_id','$price','$username','$number')";
return mysqli_query($conn,$sql);
}//模拟下单操作//库存是否大于0$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' ";
$rs=mysqli_query($conn,$sql);
$row = $rs->fetch_assoc();
if($row['number']>0){//高并发下会导致超卖 if($row['number']<$number){
return insertLog('库存不够',3,$username);
}
$order_sn=build_order_no();
//库存减少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0";
$store_rs=mysqli_query($conn,$sql);
if($store_rs){
//生成订单 insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number);
insertLog('库存减少成功',1,$username);
}else{
insertLog('库存减少失败',2,$username);
}
}else{
insertLog('库存不够',3,$username);
}?>

优化方案2

悲观锁思路

解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。
悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。
 
虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

使用MySQL的事务,锁住操作的行

<?php//优化方案2:使用MySQL的事务,锁住操作的行include('./mysql.php');//生成唯一订单号function build_order_no(){
return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}//记录日志function insertLog($event,$type=0){
global $conn;
$sql="insert into ih_log(event,type)
values('$event','$type')";
mysqli_query($conn,$sql);
}//模拟下单操作//库存是否大于0mysqli_query($conn,"BEGIN"); //开始事务$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行$rs=mysqli_query($conn,$sql);
$row=$rs->fetch_assoc();if($row['number']>0){
//生成订单 $order_sn=build_order_no();
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
$order_rs=mysqli_query($conn,$sql);
//库存减少 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
$store_rs=mysqli_query($conn,$sql);
if($store_rs){
echo '库存减少成功';
insertLog('库存减少成功');
mysqli_query($conn,"COMMIT");//事务提交即解锁 }else{
echo '库存减少失败';
insertLog('库存减少失败');
}
}else{
echo '库存不够';
insertLog('库存不够');
mysqli_query($conn,"ROLLBACK");
}?>

乐观锁思路

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

示例

如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过 程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作 员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。
乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本 ( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
1 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
2 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
3 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
4 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

优点

从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。

缺点

需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。
但是,综合来说,这是一个比较好的解决方案。
 
有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

Redis中的watch

<?php$redis = new redis();
$result = $redis->connect('127.0.0.1', 6379); echo $mywatchkey = $redis->get("mywatchkey");

$rob_total = 100; //抢购数量if($mywatchkey<=$rob_total){
$redis->watch("mywatchkey");
$redis->multi(); //在当前连接上启动一个新的事务。
//插入抢购数据
$redis->set("mywatchkey",$mywatchkey+1);
$rob_result = $redis->exec(); if($rob_result){
$redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);
$mywatchlist = $redis->hGetAll("watchkeylist"); echo "抢购成功!<br/>";
echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>"; echo "用户列表:<pre>";
var_dump($mywatchlist);
}else{
$redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao'); echo "手气不好,再抢购!";exit;
}
}?>

优化方案3

FIFO队列思路

那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。
 然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。

优化方案4

文件锁的思路

对于日IP不高或者说并发数不是很大的应用,一般不用考虑这些!用一般的文件操作方法完全没有问题。但如果并发高,在我们对文件进行读写操作时,很有可能多个进程对进一文件进行操作,如果这时不对文件的访问进行相应的独占,就容易造成数据丢失

使用非阻塞的文件排他锁

<?php//优化方案4:使用非阻塞的文件排他锁include ('./mysql.php');//生成唯一订单号function build_order_no(){ return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}//记录日志function insertLog($event,$type=0){ global $conn;
$sql="insert into ih_log(event,type)
values('$event','$type')";
mysqli_query($conn,$sql);
}


$fp = fopen("lock.txt", "w+");if(!flock($fp,LOCK_EX | LOCK_NB)){ echo "系统繁忙,请稍后再试"; return;
}//下单$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
$rs = mysqli_query($conn,$sql);
$row = $rs->fetch_assoc();if($row['number']>0){//库存是否大于0
//模拟下单操作
$order_sn=build_order_no();
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
$order_rs = mysqli_query($conn,$sql); //库存减少
$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
$store_rs = mysqli_query($conn,$sql); if($store_rs){ echo '库存减少成功';
insertLog('库存减少成功');
flock($fp,LOCK_UN);//释放锁
}else{ echo '库存减少失败';
insertLog('库存减少失败');
}
}else{ echo '库存不够';
insertLog('库存不够');
}
fclose($fp); ?>
推荐阅读:
技术:分布式唯一ID极简教程
职场:程序员职业规划
分享:2T架构师学习资料干货分享


一.从java程序中调用C函数
    java使用native关键字来修身一个本地方法
    开发流程如下:
    1.在java代码中声明一个本地方法(native关键字)
    2.运行javah以获得包含该方法的C声明的头文件(使用javah工具 用法:javah 相对路径的类名)
    3.用C实现该本地方法
    4.将代码置于共享库中 (打包一个库,windows平台是dll,linux是so)
    5.在java程序中加载该共享库(可将共享库放置在操作系统库、jre库、项目路径或者使用-Djava.library.path=共享库路径 
                                                  在静态代码块中使用System.loadlibrary(共享库名称)来加载)
    jni的开发和调用流程图解
  java本地方法和C方法的命名原则
  
  
  C++编译器注意事项
  可以使用不使用C++来实现本地方法,但是必须将本地方法的声明使用如下形式:
   extern "C”{方法声明}
   只有这样才能阻止C++编译器生成C++特有的代码

 在共享库初始化和虚拟机关闭时执行代码
     实现如下两个函数:
     //当共享库被加载时调用
  JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
  //当虚拟机关闭时调用
  JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);

  JNI_OnLoad需要返回它所支持的最低的jni版本。eg:JNI_VERSION_1_6


二.数值参数和数值返回值
    C和java之间函数交互,数字类型必须要有正确的对应关系,因为java平台独立性,其每个数据类型所占字节都是一定的,但C编译器并没有规定数字类型的位数,因此jvm使用typedef定义了和java交互的数字类型(jint,jdouble等),对应关系如下:
    

三.字符串参数
      java中的字符串是UTF-16编码的字符序列
      C的字符串是以NULL结尾的字节序列
      故两种语言中的字符串很不一样,互相转换需要使用一些转换操作,因为java中的字符串是不可变的,所以要和java交互的字符数组或者指针都必须是const类型
      jni接口当中,有两种操作字符串的函数:
      1.把java字符串=>UTF-8字节序列,实际上就是转换成byte数组,适合使用ASCII的C代码
      2.java字符串与=> UTF-16数值的数组,实际上就是转换成jchar数组,适合使用Unicode的C代码
     


      

四.获取class
   通过类名获取class
 jclass FindClass(const char *name);
 通过jobject获取class
 jclass GetObjectClass(jobject obj);
    注意:
   类引用只在本地方法返回前有效。因此不能在代码中缓存GetObjectClass和FindClass的返回值。不要将类引用保存下来供以后的方法调用时使用,尽量在每次方法调用时重新获取class。如果实在要做可以使用如下方法来长久的锁定该class的引用:
 jobject NewGlobalRef(jobject lobj) //锁定对象引用,对java对象加一个全局的强引用
 但是一定要记得使用如下方法释放class的引用
  void DeleteGlobalRef(jobject gref) //释放对象引用,类似于置NULL

五.字段和方法签名编码
     先看看数据类型签名:
     
Java 类型
符号
Boolean
Z
Byte
B
Char
C
Short
S
Int
I
Long
J
Float
F
Double
D
Void
V
objects对象
以"L"开头,以";"结尾,中间是用"/" 隔开的包及类名。
比如:Ljava/lang/String;
如果是嵌套类,则用$来表示嵌套。例如
 "(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z"
数组类型
用"["加上对应类型的简写形式进行表示。例如:
[I 表示 int [];
[L/java/lang/objects;表示Objects[],
引用类型(除基本类型的数组外)的标示最后都有个";"
  
字段的签名就是数据类型的签名
     
方法的签名是括号里放置参数,在括号后面放置返回类型,当一个函数不需要返回参数类型时,就使用"V"来表示。
比如:
"()Ljava/lang/String;"就是表示String f();
"(ILjava/lang/Class;)J"就是表示long f(int i, Class c);
"([B)V"就是表示void String(byte[] bytes);
"()V" 就表示void Func();
"(II)V" 表示 void Func(int, int);
"(Ljava/lang/String;Ljava/lang/String;)I".表示 int Func(String,String)

六.访问和设置字段

获取fieldId
 jfieldID GetFieldID(jclass clazz, const char *name,const char *sig) //字段签名看第五节
根据fieldid和class获取静态字段值
   j数据类型 GetStaticXXXField(jclass clazz, jfieldID fieldID)
 eg:
 jbyte GetStaticByteField(jclass clazz, jfieldID fieldID)
根据fieldid和object获取对象字段值
   j数据类型 GetXXXField(jobject obj , jfieldID fieldID)
 eg:
 jbyte GetByteField(jobject obj, jfieldID fieldID)
根据fieldid和class设置静态字段值
   j数据类型 SetStaticXXXField(jclass clazz, jfieldID fieldID)
 eg:
 void setStaticByteField(jclass clazz, jfieldID fieldID,jbyte val)
根据fieldid和object设置对象字段值
   void SetXXXField(jobject obj , jfieldID fieldID,J数据类型 value)
 eg:
 void SetDoubleField(jobject obj, jfieldID fieldID,jdouble val)
七.调用方法
实例方法
静态方法
八.调用构造方法创建对象
   构造方法本身也是一个方法,实际上是调用构造方法来创建对象
  Api如下:
 jobject NewObject(jclass clazz, jmethodID methodID, ...)
  比如:
  
                  // find double class
       jclass doubleClass = (*env).FindClass("java/lang/Double");
       // init method id
       jmethodID initMethod = (*env).GetMethodID(doubleClass, "<init>","(D)V");
       
       // build obj
       return (*env).NewObject(doubleClass, initMethod, value);
注意事项:init方法实际上在jvm虚拟机中实际上并不单是指构造方法,而是指对象的整个初始化节点,字段赋初值+代码块+构造方法

九.操作数组元素
 java编程语言中的所有数组类型都有与之相对应的C语言类型和C++语言类型。
 java数组类型和C语言数组类型的对应关系:
 C和C++的区别:

boolean数组的特例:
java数组API如下:
十.错误处理
C的运行系统对数组越界错误、不良指针造成的间接错误不提供任何保护。所以对于本地方法的程序员来说,处理所有出错的条件以保持java平台的完整性显得格外重要。尤其是当你的本地方法诊断出一个它无法解决的问题时,它应该将此问题上报给java虚拟机,然后,java程序会很自然的抛出一个异常。C语言没有异常,但可以使用jni中的api,  调用Throw或ThrowNew函数来创建一个新的异常对象(java异常)。当本地方法退出时,java虚拟机就会抛出该异常。
函数原型如下:
    jint Throw(jthrowable obj);
    jint ThrowNew(jclass clazz, const char *msg);
注意:C++代码有异常,但是使用C++代码抛出的异常目前还无法被jvm捕捉,未来将会实现,所以C++还是必须要try-catch,然后再调用上面的Throw函数
由于使用函数抛出异常,所以就没有检查型异常一说,异常不带感染性,所以jni还提供了ExceptionOccurred方法来检查当前是否有异常抛出
异常处理API如下:
十一.C/C++程序中使用调用API来反向调用JVM虚拟机

C程序反向调用JVM虚拟机需要更多的配置,参加日记-VS2017下的jni开发环境搭建。
调用API如下:
_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args);
初始化java虚拟机。如果成功,则返回0,否则返回JNI_ERR.(负数):
jint DestroyJavaVM()
销毁虚拟机。如果成功则返回0,否则返回一个负值。该函数必须通过一个虚拟机指针进行调用。


参考
1.java核心技术卷二 第八版
2.oracle官方API:https://docs.oracle.com/javase/9/docs/specs/jni/
3.JNI字段描述符合和方法描述符:http://www.cnblogs.com/yuyutianxia/p/4750752.html
4.Android Ndk:http://blog.csdn.net/u013718120/article/details/64919645



原文:http://blog.csdn.net/do2jiang/article/details/4545889
流水线
     流水线技术是一种将每条指令分解为多步,并让各步操作重叠,从而实现几条指令并行处理的技术。程序中的指令仍是一条条顺序执行,但可以预先取若干条指令,并在当前指令尚未执行完时,提前启动后续指令的另一些操作步骤。这样显然可加速一段程序的运行过程。 
市场上推出的各种不同的1 6位/ 3 2位微处理器基本上都采用了流水线技术。如8 0 4 8 6和P e n t i u m均使用了6步流水线结构,流水线的6步为: 
( 1 ) 取指令。C P U从高速缓存或内存中取一条指令。 
( 2 ) 指令译码。分析指令性质。 
( 3 ) 地址生成。很多指令要访问存储器中的操作数,操作数的地址也许在指令字中,也许要经过某些运算得到。 
( 4 ) 取操作数。当指令需要操作数时,就需再访问存储器,对操作数寻址并读出。 
( 5 ) 执行指令。由A L U执行指令规定的操作。 
( 6 ) 存储或"写回"结果。最后运算结果存放至某一内存单元或写回累加器A。 
       在理想情况下,每步需要一个时钟周期。当流水线完全装满时,每个时钟周期平均有一条指令从流水线上执行完毕,输出结果,就像轿车从组装线上开出来一样。P e n t i u m、Pentium Pro和Pentium II处理器的超标量设计更是分别结合了两条和三条独立的指令流水线,每条流水线平均在一个时钟周期内执行一条指令,所以它们平均一个时钟周期分别可执行2条和3条指令。 
     流水线技术是通过增加计算机硬件来实现的。例如要能预取指令,就需要增加取指令的硬件电路,并把取来的指令存放到指令队列缓存器中,使M P U能同时进行取指令和分析、执行指令的操作。因此,在1 6/3 2位微处理器中一般含有两个算术逻辑单元A L U,一个主A L U用于执行指令,另一个A L U专用于地址生成,这样才可使地址计算与其它操作重叠进行。
 
超流水线
      超级流水线以增加流水线级数的方法来缩短机器周期,相同的时间内超级流水线执行了更多的机器指令。采用简单指令以加快执行速度是所有流水线的共同特点,但超级流水线配置了多个功能部件和指令译码电路,采用多条流水线并行处理,还有多个寄存器端口和总线,可以同时执行多个操作,因此比普通流水线执行的更快,在一个机器周期内可以流出多条指令。
 
      一般而言,CPU执行一条指令需要经过以下阶段:取指->译码->地址生成->取操作数->执行->写回,每个阶段都要消耗一个时钟周期,同时每个阶段的计算结果在周期结束以前都要发送到阶段之间的锁存器上,以供下一个阶段使用。所以,每个时钟周期所消耗的时间就是由以上几个阶段中的耗时最长的那个决定的。假设耗时最长的阶段耗时为s秒,那么时钟频率就只能设计到1/s赫兹(这里不考虑阶段间信号传递的时间和锁存器的反应时间)。
      那么,要提高时钟频率,一种可能的方法就是减小每个阶段的时间消耗。其中一种最简单的办法,就是将每个阶段再进行细分成更小的步骤,同样是细分后的每个阶 段,单个阶段的运算量小了,单位耗时s也就减少,这样实际上就是提高了时钟频率。这种将标准流水线细分的技术,就是超级流水线技术。当然,流水线和超级流 水线之间并没有很明显的区别。这样的技术,虽然提高了CPU的主频,但是也带来了很大的副作用:
      首先,细分后的每一个阶段都要在其后使用锁存器锁存,因此将一个阶段细分为N的子阶段并不能让单位时间减少到s/N, 而是s/N + d, 其中d为锁存器的反应时间。这实际上就是增加了多余的时间消耗。
      其次,随着流水线级数的加深,一旦分支预测出现错误,会导致CPU中大量的指令作废,这样的消耗是十分巨大的。
      以上原因,也就是什么Pentium IV具有31级的流水线,指令的执行效率却赶不上只有14级流水线的Pentium M
 
超标量
  超标量(superscalar)是指在CPU中有一条以上的流水线,并且每时钟周期内可以完成一条以上的指令,这种设计就叫超标量技术。 其实质是以空间换取时间。而超流水线是通过细化流水、提高主频,使得在一个机器周期内完成一个甚至多个操作,其实质是以时间换取空间。
 
 
 
 
 
 
原文:http://www.wlline.cn/wenda/318.html
 
超级流水线
超级流水线(SuperPipeline)又叫做深度流水线,它是提高cpu速度通常采取的一种技术。CPU处理指令是通过Clock来驱动的,每个clock完成一级流水线操作。
      超级流水线(SuperPipeline)又叫做深度流水线,它是提高cpu速度通常采取的一种技术。CPU处理指令是通过Clock来驱动的,每个clock完成一级流水线操作。每个周期所做的操作越少,需要的时间久越短,时间越短,频率就可以提得越高。超级流水线就是将cpu处理指令是得操作进一步细分,增加流水线级数来提高频率。频率高了,当流水线开足马力运行时平均每个周期完成一条指令(单发射情况下),这样cpu处理得速度久提高了。当然,这是理想情况下,一般是流水线级数越多,重叠执行的执行就越多,那么发生竞争冲突得可能性就越大,对流水线性能有一定影响现在很多cpu都是将超标量和超级流水线技术一起使用,例如pentiumIV,流水线达到20级,频率最快已经超过3GHZ。教科书上用于教学的经典MIPS只有5级流水。
超标量
将一条指令分成若干个周期处理以达到多条指令重叠处理,从而提高cpu部件利用率的技术叫做标量流水技术。超级标量是指cpu内一般能有多条流水线,这些流水线能够并行处理。在单流水线结构中,指令虽然能够重叠执行,但仍然是顺序的,每个周期只能发射(issue)或退休(retire)一条指令。超级标量结构的cpu支持指令级并行,每个周期可以发射多条指令(2-4条居多)。可以使得cpu的IPC(InstructionPerClock)>,从而提高cpu处理速度。超级标量机能同时对若干条指令进行译码,将可以并行执行的指令送往不同的执行部件,在程序运行期间,由硬件(通常是状态记录部件和调度部件)来完成指令调度。超级标量机主要是借助硬件资源重复(例如有两套译码器和ALU等)来实现空间的并行操作。熟知的pentium系列(可能是p-II开始),还有SUNSPARC系列的较高级型号,以及MIPS若干型号等都采用了超级标量技术。
超长指令字
超常指令字(VLIW:VeryLongInstructionWord)是由美国Yale大学教授Fisher提出的。它有点类似于超级标量,是一条指令来实现多个操作的并行执行,之所以放到一条指令是为了减少内存访问。通常一条指令多达上百位,有若干操作数,每条指令可以做不同的几种运算。那些指令可以并行执行是由编译器来选择的。通常VLIW机只有一个控制器,每个周期启动一条长指令,长指令被分为几个字段,每个字段控制相应的部件。由于编译器需要考虑数据相关性,避免冲突,并且尽可能利用并行,完成指令调度,所以硬件结构较简单。   
VLIW机器较少,可能不太容易实现,业界比较有名的VLIW公司之一是Transmeta,在加州硅谷SantaClara(硅谷圣地之一,还有SanJose,PaloAlto)。它做的机器采用X86指令集,VLIW实现,具体资料可以去访问公司的网站。
向量机
平时接触的计算机都是标量机,向量机都是大型计算机,一般用于军事工业,气象预报,以及其他大型科学计算领域,这也说明了向量机都很贵。国产的银河计算机就是向量机普通的计算机所做的计算,例如加减乘除,只能对一组数据进行操作,被称为标量运算。向量运算一般是若干同类型标量运算的循环。向量运算通常是对多组数据成批进行同样运算,所得结果也是一组数据。很多做科学计算的大(巨)型机都是向量机,例如国产银河。
SIMD技术
单指令多数据(SingleInstructionMultipleData)简称SIMD。SIMD结构的CPU有多个执行部件,但都在同一个指令部件的控制下。SIMD在性能优势呢:以加法指令为例,单指令单数据(SISD)的CPU对加法指令译码后,执行部件先访问内存,取得第一个操作数;之后再一次访问内存,取得第二个操作数;随后才能进行求和运算。而在SIMD型CPU中,指令译码后几个执行部件同时访问内存,一次性获得所有操作数进行运算。这个特点使得SIMD特别适合于多媒体应用等数据密集型运算。AMD公司的3DNOW!技术其实质就是SIMD,这使K6-2处理器在音频解码、视频回放、3D游戏等应用中显示出优异性能。


原文地址:http://m.mydrivers.com/baidu/newsview.aspx?tid=546656

说到超线程技术,大家应该都不陌生了,Intel早在2002年推出的Northwood奔腾4 HT处理器就把这一技术带入到消费级市场。
虽然随后的酷睿2处理器上超线程被抛弃,不过到了2008年推出的Nehalem架构Core i7处理器又把超线程技术带回到市场上,并一直沿用至今。
现在的Core i7/i3、部分奔腾与Atom、还有移动版的双核Core i5与Core M处理器都有超线程技术。
AMD最新推出的Ryzen系列除最低端的Ryzen 3外都带有SMT多线程技术,与Intel的超线程技术类似。
在了解超线程是什么鬼之前我们要先知道线程是什么。
Thread线程是操作系统能够进行运算调动的最小单位,它被包含在进程之中,是进程中的实际运作单位,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
Intel Hyper-Threading Technology(超线程技术)的学术名字是Simulate MultiThreading(SMT,同步多线程技术)。
SMT是超线程技术的学术名称,这两个东西是完全一样的。这技术的引入是为了更好的利用CPU的空闲资源。
Intel从奔腾处理器就开始引入超标量、乱序运行、大量的寄存器及寄存器重命名、多指令解码器、预测运行等特性,这些特性的原理是让CPU拥有大量资源,并可以预先运行及平行运行指令,以增加指令运行效率。
可是在现实中这些资源经常闲置,为了有效利用这些资源,就干脆再增加一些资源来运行第二个线程,让这些闲置资源可运行另一个线程。
什么是多线程?
MultiThreading多线程这个概念有些暧昧,多线程可以指在一个CPU核心上同时执行多个线程,也可以是多个任务,尽管在同一个核心内执行,但是它们之间完全分离。
多线程在概念上类似抢占式多任务处理,但是在现在的超标量处理器中以线程级来实现。
多线程有两个主要实现方法,一个是Temporal MultiThreading时间多线程,另一个则是Simulate MultiThreading同步多线程,时间多线程还可以进一步分为Fine-Grained MultiThreading细粒度多线程与Coarse-Grained MultiThreading粗粒度多线程。
各种多线程技术
CMT粗粒度多线程是最简单的多线程技术,当单一执行线程遇到长时间的延迟,如Cache Missed时,就进行线程切换,直到原线程等待的操作完成,才切换回去。
FMT细粒度多线程比CMT粗粒度多线程复杂一些,它随时可以在每个时钟周期内切换多个线程,以追求最大的输出能力,当然,随时可以切换也是有代价的,它拉长了每个执行线程的平均执行时间。
CMT和FMT都没有在消费级处理器上面使用,Intel与AMD处理器上使用的都是SMT同步多线程,不过NVIDIA与AMD的GPU都有使用FMT技术。
SMT同步多线程具有多个执行单元,CMT和FMT都是在单个执行单元下的技术,不同的线程在指令级别上并不是真正的“并行”,而SMT则具有多个执行单元,同一时间内可以同时执行多个指令,可以充分发掘超标量处理器的潜力,因此SMT具有最大的灵活性和资源利用率,不过处理器也更复杂。
不过现在的消费级处理器都是超标量处理器,所以要支持SMT其实在架构上不用太多改变:所需的主要添加是在一个周期中从多个线程获取指令的能力,以及一个更大的寄存器文件来保存来自多个线程的数据。
并发线程的数量可以由芯片设计者决定。常见模式是每个CPU核心有两个并发线程,但一些处理器的每个核心支持最多八个并发线程。
工作原理
对于单一处理器核心来说来说,虽然也可以每秒钟处理成千上万条指令,但是在某一时刻,只能够对一条指令(单个线程)进行处理,超线程技术能够把一个物理处理器在软件层变成两个逻辑处理器,可以使处理器在某一时刻,同步并行处理更多指令和数据(多个线程),当然了实际效能不可实现双倍提升,毕竟干活的核心只有一个。
逻辑双核与物理双核
可以这样说,超线程是一种可以将CPU内部暂时闲置处理资源充分“调动”起来的技术,奔腾4 HT处理器多加入了一个逻辑处理单元,这让CPU可以同时执行多个程序而共享一颗CPU内的资源,如:ALU、FPU、缓存等,当两个线程都同时需要某一个资源时,其中一个要暂时停止,并让出资源,直到这些资源闲置后才能继续,因此超线程的性能并不等于两颗CPU的性能。
四个框框(双核四线程)的奔腾EE 840在单核还是绝对主流的2005年看起来就夸张
在发布奔腾4 HT处理器的时候Intel说过,超线程技术只增加了5%的芯片面积,就可换来15%~30%的性能提升,而后来的Nehalem架构带来了全新的超线程技术,得益于指令集分制预测技术与较短的流水线。
它拥有比奔腾4好得多的效能,再加上整合了内存控制器让其拥有更大的内存带宽,还有更大的缓存,这样就更能够有效的发挥超线程的作用。
Nehalem的超线程可以在增加很少能耗的情况下,让性能提升20-30%,后续每一代虽然都有一些小修改,不过基本上都是Nehalem架构的延续。
超线程的作用
其实在Intel刚把超线程技术推向消费级市场的时候市场反应不怎么好,因为当时的操作系统和软件都没有对多线程技术进行优化,多数软件都是以单线程运行,超线程的优势非但显露不出来反而会因为另一个虚拟处理器抢占资源导致运行起来比没超线程的处理器更慢。
这些问题随着这些年来操作系统和软件逐步对多核多线程进行优化得到改变,特别是Windows 10系统对多线程优化相当好,操作系统的调度器设置更为科学,多核心多线程的负载更为平均。
你在Windows 10系统下打开任务管理器会发现,不论物理核心还逻辑核心的负载都相当平衡,除非是人为指定负载线程,否则很少会出现之前Windows 7那样单核负载多核围观的惨状。
Windows 10下CPU的负载相当平均
至于超线程的作用其实还是很明显的,之前我们对比测试过Core i7-6700K和Core i5-7600K,他们俩的四核与单核Boost频率是相同的,都是4.0GHz与4.2GHz,区别就在于超线程的有无和L3缓存的大小了,至于Skylake与Kaby Lake两者是没有性能上的差别的。
有超线程的Core i7-6700K多线程性能比Core i5-7600K好19%左右,然而超线程技术在提升处理器的利用率增大吞吐量的同时也稍微增加了单个线程的延时,如果只看单线程能力的话Core i5-7600K其实比Core i7-6700K还好2.8%左右,然而降低这么一点单线程性能让多线程性能提升这么多这其实是很划算的。
太长不看版:
简单地说,超线程技术是一个很好的提升核心利用率的东西,将闲置处理资源充分调动起来,增强核心并行运算性能,在操作系统中一颗物理CPU能当做多颗CPU来使用。
超线程有什么好处呢:
- 有效提升CPU利用率
- 改善计算机的性能
- 提高系统可靠性
比如奔腾G4560这种双核在拥有超线程之后性能暴增,在低端入门市场相对受欢迎,双核四线程的处理器能够对应大多数轻量级日常应用。
当然随着核心数目增多超线程的作用就越弱,特别是那些八核或者核心数更多的处理器,十六个框框看起来很爽然而实际上用起来很多线程都是空载的,大多数消费者与应用都没法很好的利用这么多线程的性能,目前只有视频和3D渲染软件和压缩软件有能做到,软件还是制约硬件性能的最大因素。
另外,超线程技术需要CPU支持,需要主板支持,需要操作系统支持,还需要应用软件支持,缺一不可,否则就玩不转了。



一.概述
    如何定位系统问题:
          知识、经验是关键基础,
          数据是依据,
         工具是运行知识处理数据的手段
   数据包括:运行日志、异常堆栈、GC日志、线程快照、堆转储快照等
   后面两节是工具

二.JDK的命令行工具
    1.jps:虚拟机进程状况工具
      可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(main函数所在类)名称以及这些进程的本地虚拟机唯一ID(LVMID与操作系统进程ID,也就是PID是一致的)。
       
     2.jstat:虚拟机统计信息监视工具
         jstat是用于监视虚拟机各种运行状态信息的命令行工具。可以显示本地或者远程虚拟机(开启RMI支持)进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
         
     
     3.jinfo:java配置信息工具
         jinfo命令行工具的作用是实时的查看和调整虚拟机各项参数,包括未显示指定参数的默认值。
         
     4.jmap:java内存映象工具
         jmap命令行工具可用于生成堆转储快照(还可以查询finalize执行队列,java堆和永久代的信息)。

     5.jhat:虚拟机堆转储快照分析工具
         jhat用于分析堆转储快照,一般和jmap搭配使用。但使用得不多。

     6.jstack:java堆栈跟踪工具
        jstack命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump文件或者javacore文件)。
       线程快照就是当前虚拟机的每一条线程正在执行的方法堆栈的集合,生成快照的主要目的是定位线程出现长时间停顿的原因,比如线程间死锁、死循环、请求外部资源导致的长时间等待等。

     7.HSDIS:JIT生成代码反汇编
        HSDIS工具是Hotspot虚拟机JIT编译代码的反汇编插件,它的作用是让Hotspot的-XX:+PrintAssembly指令动态生成的本地代码还原为汇编代码输出。
        汇编代码可以让我们更好的阅读指令行为,java虚拟机规范只是字节码上的,借助该工具,我们可以在指令层面进行分析。

三.JDK的可视化工具
    1.jconsole:java监视与管理控制台
       有点老了

    
    2.visualvm:多合一故障处理工具
       强烈推荐
  
    3.JMC:java任务控制工具
      Oracle Java Mission Control 是一个用于对 Java 应用程序进行管理、监视、概要分析和故障排除的工具套件,附有飞行记录器的强大功能,强烈推荐

 


性能调优案例
一.高性能硬件上的程序部署策略
       高性能硬件上部署程序的两种主要方式:
      1.通过64位jdk来使用大内存
        对于用户交互性强、对停顿时间敏感的系统,可以给java虚拟机分配超大堆的前提是有把握应用程序的Full GC频率控制得足够低,至少低得不会影响用户使用,比如十几个小时或者一天才出现一次Full GC,这样可以通过深夜执行定时任务来触发full gc或者自动重启应用程序来保持内存可用性。
        控制Full GC频率的关键是看应用中的绝大多数对象是否能符合“朝生夕灭”的原则,即大多数对象的生存时间不应该太长,尤其是不能有成批量、长生存时间的大对象产生,这样才能保证老年代空间稳定,对于B/S应用,大多数对象都是请求级,只要代码合理,基本都能满足要求。       
        对于64位jdk,还可能面临如下问题:
          1.在大内存中进行垃圾回收造成的长时间停顿(避免full gc)
          2.64位jdk的性能较低
          3.jvm内存逼近物理内存极限会导致dump文件也无法分析
          4.64位jvm的指针膨胀问题(可以使用指针压缩)

      2.适用若干个32位虚拟机建立逻辑集群来利用硬件资源
         集群方案的缺点:


二.集群间的同步导致的内存溢出
       略
    

三.堆外内存导致的内存溢出
       当发现堆内存和永久代内存的使用平稳,但仍然抛出OOM,而且dump中没有多余信息,那很有可能是堆外内存溢出。
       如:直接内存和元空间,可以通过查看异常堆栈日志找到原因。


四.外部命令导致系统缓慢
       Runtime.getRuntime().exec()方法的频繁使用,导致过多进程占用cpu和内存。
       解决方案:
       1.优化程序,选择另外的方案
       2.消息队列
       3.任务作业

五.服务器jvm进程崩溃
       虚拟机直接崩溃,这种情况很少见。碰上这种情况需要检查代码、运行情况、是否外部系统交互,综合分析来得出结论。
       如果是外部系统的问题,且本系统是同步调用,那最好修改为生产者-消费者模式,或者直接消息队列。


六.不恰当数据结构导致内存占用过大
        使用了不恰当数据结构来处理巨大数据,内存占用大,使用率过低。
        可以增加内存容量来避免OOM。取消survivor区来暂缓停顿。
        最根本方法还是优化数据结构。

七.由windows虚拟内存导致的长时间停顿
    windows虚拟内存导致GC时需要从虚存换页到实际内存,这儿存在时间差,可以避免使用虚存。


  eclipse性能调优实战
是是

JVM的指针与操作系统和cpu的指针是一个道理。
64位和32位JVM的几个知识点:
     1.32位的JVM的指针是32位,即4个字节;64位的JVM的指针是64位,即8个字节。
     2.64位的JVM只能运行在64位机器上;32位的JVM可以运行在64位或者32位机器上。
     3.实际上一个JVM就是一个操作系统进程。
     4.32位指针的寻址范围是4GB, 64位指针的寻址范围是4GB*2的32次方(百亿亿字节)。
     5.32位JVM存在一个问题,32位JVM的内存最大限制是4GB。如果32位JVM运行在32位物理机上,因为内核需要占用一部分内存,所以往往jvm的内存无法达到4G,如:32位的windows系统的进程的内存最大限制为2GB。
     6.64位的JVM存在一个问题,64位JVM的内存最大限制非常大,其可以表示的内存远远高于当前企业服务器的内存大小,使用64位指针来存储对象引用是非常浪费内存的。
jvm中使用指针访问对象的知识点:
     1.64位的CPU使用的虚拟地址是64位的, 访问内存时, 必须使用64位的指针访问内存对象
     2.java对象是分配于具体的某个内存位置的, 对其访问必须使用64位地址
     3.对java对象内的引用字段进行访问时, 必须经过虚拟机这一层,操作某个对象引用不管是getfield还是putfield, 都是由虚拟机来执行. 或者简单来说, 要改变java对象某个引用字段, 必须经过虚拟机的参与。
     4.使用指针压缩,那到底是怎么个压缩法呢?公式如下:64位地址=起始地址+(压缩地址<<3位),因为jvm中的对象都是8字节对齐,所以地址的后三位都是0,故可以通过右移三位来压缩,从而多利用了3位,意味着32位指针可寻址内存变为了32G,够用了。
所以对于受jvm管理的对象,大部分都可以经过jvm层面进行一种指针压缩的技术,从而实现jvm和cpu交互的时候是64位地址,但是实际上jvm存储的地址是64位,这就是指针压缩技术,指针压缩技术通过几个简单的运行可以实现将64位指针压缩到32位,从而有效解决64位jvm因为指针膨胀导致的内存开销。
可以使用-xx:+/-UseCompressOops在64位的JVM上开启指针压缩

摘一篇文章:

HotSpot JVM 中的对象指针压缩

原文:https://wiki.openjdk.java.net/display/HotSpot/CompressedOops

什么是一般对象指针?

一般对象指针(oop, ordinary object pointer)是HotSpot虚拟机的一个术语,表示受托管的对象指针。它的大小通常和本地指针是一样的。Java应用程序和GC子系统会非常小心地跟踪这些受托管的指针,以便在销毁对象时回收内存空间,或是在对空间进行整理时移动(复制)对象。
在一些从Smalltalk和Self演变而来的虚拟机实现中都有一般对象指针这个术语,包括:
部分系统中会使用小整型(smi, small integers)这个名称,表示一个指向30位整型的虚拟指针。这个术语在Smalltalk的V8实现中也可以看到。

为什么需要压缩?

LP64系统中,指针需要使用64位来表示;ILP32系统中则只需要32位。在ILP32系统中,堆内存的大小只能支持到4Gb,这对很多应用程序来说是不够的。在LP64系统中,所有应用程序运行时占用的空间都会比ILP32大1.5倍左右,这是因为指针占用的空间增加了。虽然内存是比较廉价的,但网络带宽和缓存容量是紧张的。所以,为了解决4Gb的限制而增加堆内存的占用空间,就有些得不偿失了。
在x86芯片中,ILP32模式可用的寄存器数量是LP64模式的一半。SPARC没有此限制;RISC芯片本来就提供了很多寄存器,LP64模式下会提供更多。
压缩后的一般对象指针在使用时需要将32位整型按因数8进行扩展,并加到一个64位的基础地址上,从而找到所指向的对象。这种方法可以表示四十亿个对象,相当于32Gb的堆内存。同时,使用此法压缩数据结构也能达到和ILP32系统相近的效果。
我们使用解码来表示从32位对象指针转换成64位地址的过程,其反过程则称为编码

什么情况下会进行压缩?

运行在ILP32模式下的Java虚拟机,或在运行时将UseCompressedOops标志位关闭,则所有的对象指针都不会被压缩。
如果UseCompressedOops是打开的,则以下对象的指针会被压缩:
HotSpot VM中,用于表示Java类的数据结构是不会压缩的,这部分数据都存放在永久代(PermGen)中。
在解释器中,一般对象指针也是不压缩的,包括JVM本地变量和栈内元素、调用参数、返回值等。解释器会在读取堆内对象时解码对象指针,并在存入时进行编码。
同样,方法调用序列(method calling sequence),无论是解释执行还是编译执行,都不会使用对象指针压缩。
在编译后的代码中,对象指针是否压缩取决于不同的优化结果。优化后的代码可能会将压缩后的对象指针直接从一处搬往另一处,而不进行编解码操作。如果芯片(如x86)支持解码,那在使用对象指针时就不需要自行解码了。
所以,以下数据结构在编译后的代码中既可以是压缩后的对象指针,也可能是本地地址:
  • 寄存器或溢出槽(spill slot)中的数据
  • 对象指针映射表(GC映射表)
  • 调试信息
  • 嵌套在机器码中的对象指针(在非RISC芯片中支持,如x86)
  • nmethod常量区(包括那些影响到机器码的重定位操作)
在HotSpot JVM的C++代码部分,对象指针压缩与否反映在C++的静态类型系统中。通常情况下,对象指针是不压缩的。具体来说,C++的成员函数在操作本地代码传递过来的指针时(如this),其执行过程不会有什么不同。JVM中的部分方法则提供了重载,能够处理压缩和不压缩的对象指针。
重要的C++数据不会被压缩:
  • C++对象指针(this
  • 受托管指针的句柄(Handle类型等)
  • JNI句柄(jobject类型)
C++在使用对象指针压缩时(加载和存储等),会以narrowOop作为标记。

使用压缩寻址

以下是使用对象指针压缩的x86指令示例:
1
2
3
4
5
6
7
8
! int R8; oop[] R9; // R9是64位
! oop R10 = R9[R8]; // R10是32位
! 从原始基址指针加载压缩对象指针:
movl R10, [R9 + R8<<3 + 16]
! klassOop R11 = R10._klass; // R11是32位
! void* const R12 = GetHeapBase();
! 从压缩基址指针加载klass指针:
movl R11, [R12 + R10<<3 + 8]
以下sparc指令用于解压对象指针(可为空):
1
2
3
4
5
6
7
8
! java.lang.Thread::getThreadGroup@1 (line 1072)
! L1 = L7.group
ld [ %l7 + 0x44 ], %l1
! L3 = decode(L1)
cmp %l1, 0
sllx %l1, 3, %l3
brnz,a %l3, .+8
add %l3, %g6, %l3 ! %g6是常量堆基址
输出中的注解来自PrintAssembly插件

空值处理

32位零值会被解压为64位空值,这就需要在解码逻辑中加入一段特殊的逻辑。或者说可以默认某些压缩对象指针肯定不会空(如klass的属性),这样就能使用简单一些的编解码逻辑了。
隐式空值检测对JVM的性能至关重要,包括解释执行和编译执行的字节码。对于一个偏移量较小的对象指针,如果基址指针为空,那很有可能造成系统崩溃,因为虚拟地址空间的前几页通常是没有映射的。
对于压缩对象指针,我们可以用一种类似的技巧来欺骗它:将堆内存前几页的映射去除,如果解压出的指针为空(相对于基址指针),仍可以用它来做加载和存储的操作,隐式空值检测也能照常运行。

对象头信息

对象头信息通常包含几个部分:固定长度的标志位;klass信息;如果对象是数组,则包含一个32位的信息,并可能追加一个32位的空隙进行对齐;零个或多个实例属性,数组元素,元信息等。(有趣的是,Klass的对象头信息包含了一个C++的虚拟方法表
上述追加的32位空隙通常也可用于存储属性信息。
如果UseCompressedOops关闭,标志位和klass都是正常长度。对于数组,32位空隙在LP64系统中总是存在;而ILP32系统中,只有当数组元素是64位数据时才存在这个空隙。
如果UseCompressedOops打开,则klass是32位的。非数组对象在klass后会追加一个空隙,而数组对象则直接开始存储元素信息。

零基压缩技术

压缩对象指针(narrow-oop)是基于某个地址的偏移量,这个基础地址(narrow-oop-base)是由Java堆内存基址减去一个内存页的大小得来的,从而支持隐式空值检测。所以一个属性字段的地址可以这样得到:
1
<narrow-oop-base> + (<narrow-oop> << 3) + <field-offset>.
如果基础地址可以是0(Java堆内存不一定要从0偏移量开始),那么公式就可以简化为:
1
(<narrow-oop << 3) + <field-offset>
理论上说,这一步可以省去一次寄存器上的加和操作。而且使用零基压缩技术后,空值检测也就不需要了。
之前的解压代码是:
1
2
3
4
if (<narrow-oop> == NULL)
<wide_oop> = NULL
else
<wide_oop> = <narrow-oop-base> + (<narrow-oop> << 3)
使用零基压缩后,只需使用移位操作:
1
<wide_oop> = <narrow-oop> << 3
零基压缩技术会根据堆内存的大小以及平台特性来选择不同的策略:
  1. 堆内存小于4Gb,直接使用压缩对象指针进行寻址,无需压缩和解压;
  2. 堆内存大于4Gb,则尝试分配小于32Gb的堆内存,并使用零基压缩技术;
  3. 如果仍然失败,则使用普通的对象指针压缩技术,即narrow-oop-base



Go语言是一个简单却蕴含深意的语言。但是,即便号称是最简单的C语言,都能总结出一本《C陷阱与缺陷》,更何况Go语言呢。Go语言中的许多坑其实并不是因为Go自身的问题。一些错误你再别的语言中也会犯,例如作用域,一些错误就是对因为 Go 语言的特性不了解而导致的,例如 range。
其实如果你在学习Go语言的时候去认真地阅读官方文档,百科,邮件列表或者其他的类似 Rob Pike 的名人博客,报告,那么本文中提到的许多坑都可以避免。但是不是每个人都会从基础学起,例如译者就喜欢简单粗暴地直接用Go语言写程序。如果你也像译者一样,那么你应该读一下这篇文章:这样可以避免在调试程序时浪费过多时间。
本文将50个坑按照使用使用范围和难易程度分为以下三个级别:“新手入门级”,“新手深入级”,“新手进阶级”。

错误信息:
/tmp/sandbox826898458/main.go:6: syntax error: unexpected semicolon or newline before {
修正代码:
package main

import "fmt"

func main() {
fmt.Println("works!")
}

未使用已定义的变量

级别:新手入门级
如果代码中有未使用的变量,那个代码编译的时候就会报错。Go要求在代码中所有声明的变量都需要被用到,当然,全局变量除外。
函数的参数也可以只被声明,不被使用。
对于未声明变量的调用同样会导致编译失败。和C语言一样,Go编译器也是个女人,他说什么你都要尽力满足。
出错代码:
package main

var gvar int //not an error

func main() {
var one int //error, unused variable
two := 2 //error, unused variable
var three int //error, even though it's assigned 3 on the next line
three = 3

func(unused string) {
fmt.Println("Unused arg. No compile error")
}("what?")
}
错误信息:
/tmp/sandbox473116179/main.go:6: one declared and not used /tmp/sandbox473116179/main.go:7: two declared and not used /tmp/sandbox473116179/main.go:8: three declared and not used
修正代码:
package main

import "fmt"

func main() {
var one int
_ = one

two := 2
fmt.Println(two)

var three int
three = 3
one = three

var four int
four = four
}
当然,你也可以考虑删除那些没有使用的变量。

未使用的包

级别:新手入门级
当import一个包之后,如果不使用这个包,或者这个包中的函数/接口/数据结构/变量,那么将会编译失败。
如果真的确认要引入变量但是不使用的话,我们可以用“”标识符坐标记,避免编译失败。“”标识符表示为了得到这些包的副作用而引入这些包。
出错代码:
package main

import (
"fmt"
"log"
"time"
)

func main() {
}
错误信息:
/tmp/sandbox627475386/main.go:4: imported and not used: "fmt"
/tmp/sandbox627475386/main.go:5: imported and not used: "log"
/tmp/sandbox627475386/main.go:6: imported and not used: "time"
修正代码
package main

import (
_ "fmt"
"log"
"time"
)

var _ = log.Println

func main() {
_ = time.Now
}

只能在函数内部使用简短的变量声明

级别:新手入门级
出错代码:
package main

myvar := 1 //error

func main() {
}
错误信息:
/tmp/sandbox265716165/main.go:3: non-declaration statement outside function body

修正代码:
package main

var myvar = 1

func main() {
}

无法使用精简的赋值语句对变量重新赋值

级别:新手入门级
不能使用精简的赋值语句重新赋值单个变量,但是可以使用精简的赋值语句同时赋值多个变量。
并且,重定义的变量必须写在同一个代码块。
错误信息:
package main

func main() {
one := 0
one := 1 //error
}
错误信息:
/tmp/sandbox706333626/main.go:5: no new variables on left side of :=
修正代码:
package main

func main() {
one := 0
one, two := 1,2

one,two = two,one
}

隐式变量(作用域)

级别:新手入门级
和 C 语言一样,Go 语言也有作用于,一个变量的作用范围仅仅是一个代码块。虽然精简的赋值语句很简单,但是注意作用域。
package main

import "fmt"

func main() {
x := 1
fmt.Println(x) //打印 1
{
fmt.Println(x) //打印 1
x := 2
fmt.Println(x) //打印 2
}
fmt.Println(x) //打印 1 ( 不是 2)
}
甚至对于有经验的开发者来说,这也是个不注意就会掉进去的深坑。

除非特别指定,否则无法使用 nil 对变量赋值

级别:新手入门级
nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”。但是如果不特别指定的话,Go 语言不能识别类型,所以会报错。
错误信息:
package main

func main() {
var x = nil //error

_ = x
}
错误信息:
/tmp/sandbox188239583/main.go:4: use of untyped nil
修正代码:
package main

func main() {
var x interface{} = nil

_ = x
}

Slice 和 Map 的 nil 值

级别:新手入门级
初始值为 nil 的 Slice 是可以进行“添加”操作的,但是对于 Map 的“添加”操作会导致运行时恐慌。( ﹁ ﹁ ) 恐慌。
修正代码:
package main

func main() {
var s []int
s = append(s,1)
}

错误信息:
package main

func main() {
var m map[string]int
m["one"] = 1 //error

}

Map 定长

级别:新手入门级
创建 Map 的时候可以指定 Map 的长度,但是在运行时是无法使用 cap() 功能重新指定 Map 的大小,Map 是定长的。
错误信息:
package main

func main() {
m := make(map[string]int,99)
cap(m) //error
}
错误信息:
/tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]int) for cap

字符串无法为 nil

级别:新手入门级
所有的开发者都可能踩的坑,在 C 语言中是可以 char *String=NULL,但是 Go 语言中就无法赋值为 nil。
错误信息:
package main

func main() {
var x string = nil //error

if x == nil { //error
x = "default"
}
}
Compile Errors:
/tmp/sandbox630560459/main.go:4: cannot use nil as type string in assignment /tmp/sandbox630560459/main.go:6: invalid operation: x == nil (mismatched types string and nil)
修正代码:
package main

func main() {
var x string //defaults to "" (zero value)

if x == "" {
x = "default"
}
}

参数中的数组

Array Function Arguments
级别:新手入门级
对于 C 和 C++ 开发者来说,数组就是指针。给函数传递数组就是传递内存地址,对数组的修改就是对原地址数据的修改。但是 Go 语言中,传递的数组不是内存地址,而是原数组的拷贝,所以是无法通过传递数组的方法去修改原地址的数据的。
package main

import "fmt"

func main() {
x := [3]int

func(arr [3]int) {
arr[0] = 7
fmt.Println(arr) //prints [7 2 3]
}(x)

fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3])
}
如果需要修改原数组的数据,需要使用数组指针(array pointer)。
package main

import "fmt"

func main() {
x := [3]int

func(arr *[3]int) {
(*arr)[0] = 7
fmt.Println(arr) //prints &[7 2 3]
}(&x)

fmt.Println(x) //prints [7 2 3]
}
或者可以使用 Slice,
package main

import "fmt"

func main() {
x := []int

func(arr []int) {
arr[0] = 7
fmt.Println(arr) //prints [7 2 3]
}(x)

fmt.Println(x) //prints [7 2 3]
}

使用 Slice 和 Array 的 range 会导致预料外的结果

级别:新手入门级
如果你对别的语言中的 for in  foreach 熟悉的话,那么 Go 中的 range 使用方法完全不一样。因为每次的 range 都会返回两个值,第一个值是在 Slice 和 Array 中的编号,第二个是对应的数据。
出错代码:
package main

import "fmt"

func main() {
x := []string{"a","b","c"}

for v := range x {
fmt.Println(v) //prints 0, 1, 2
}
}
修正代码:
package main

import "fmt"

func main() {
x := []string{"a","b","c"}

for _, v := range x {
fmt.Println(v) //prints a, b, c
}
}

Slive 和 Array 维度是一维

级别:新手入门级
Go 看上去支持多维的 Array 和 Slice,但是其实不然。尽管可以创建 Array 的 Array,也可以创建 Slice 的 Slice。对于依赖多维 Array 的计算密集型的程序,无论是从性能还是复杂程度,Go 都不是最佳选择。
当然,如果你选择创建嵌套的 Array 与嵌套的 Slice,那么你就得自己负责进行索引、进行下表检查、以及 Array 增长时的内存分配。嵌套 Slice 分为两种,Slice 中嵌套独立的 Slice,或者 Slice 中嵌套共享数据的 Slice。
使用嵌套的独立 Slice 创建多维的 Array 需要两步。第一步,创建外围 Slice,然后分配每个内部的 Slice。内部的 Slice 是独立的,可以对每个单独的内部 Slice 进行缩放。
package main

func main() {
x := 2
y := 4

table := make([][]int,x)
for i:= range table {
table[i] = make([]int,y)
}
}
使用嵌套、共享数据的 Slive 创建多维 Array 需要三步。第一,创建数据“容器”,第二部,创建外围 Slice,第三部,对内部的 Slice 进行初始化。
package main

import "fmt"

func main() {
h, w := 2, 4

raw := make([]int,h*w)
for i := range raw {
raw[i] = i
}
fmt.Println(raw,&raw[4])
//prints: [0 1 2 3 4 5 6 7] <ptr_addr_x>

table := make([][]int,h)
for i:= range table {
table[i] = raw[i*w:i*w + w]
}

fmt.Println(table,&table[1][0])
//prints: [[0 1 2 3] [4 5 6 7]] <ptr_addr_x>
}
Go 语言也有对于支持多维 Array 和 Slice 的提案,不过不要期待太多。Go 语言官方将这些需求分在“低优先级”组中。

试图访问不存在的 Map 键值

级别:新手入门级
并不能在所有情况下都能通过判断 map 的记录值是不是 nil 判断记录是否存在。在 Go 语言中,对于“零值”是 nil 的数据类型可以这样判断,但是其他的数据类型不可以。简而言之,这种做法并不可靠(例如布尔变量的“零值”是 false)。最可靠的做法是检查 map 记录的第二返回值。
错误代码:
package main

import "fmt"

func main() {
x := map[string]string{"one":"a","two":"","three":"c"}

if v := x["two"]; v == "" { //incorrect
fmt.Println("no entry")
}
}
修正代码:
package main

import "fmt"

func main() {
x := map[string]string{"one":"a","two":"","three":"c"}

if _,ok := x["two"]; !ok {
fmt.Println("no entry")
}
}

String 不可变

级别:新手入门级
对于 String 中单个字符的操作会导致编译失败。String 是带有一些附加属性的只读的字节片(Byte Slices)。所以如果想要对 String 操作的话,应当使用字节片操作,而不是将它转换为 String 类型。
错误信息:
package main

import "fmt"

func main() {
x := "text"
x[0] = 'T'

fmt.Println(x)
}
错误信息:
/tmp/sandbox305565531/main.go:7: cannot assign to x[0]
修正代码:
package main

import "fmt"

func main() {
x := "text"
xbytes := []byte(x)
xbytes[0] = 'T'

fmt.Println(string(xbytes)) //prints Text
}
注意这里的操作并不就是最正确的操作,因为有些字符可能会存储在多个字节中。如果你的开发情景有这种情况的话,需要先把 String 转换为 rune 格式。即便是 rune,一个字符也可能会保存在多个 rune 中,因此 Go String 也表现为字节序列(String Sequences)。

String 与 Byte Slice 的转换

级别:新手入门级
当将 String 类型和 Byte Slice 类型互相转化的时候,得到的新数据都是原数据的拷贝,而不是原数据。类型转化和切片重组(Resliciing)不一样,切片重组后的变量仍然指向原变量,而类型转换后的变量指向原变量的拷贝。
Go 语言已经对 []byte 和 String 类型的互相转化做了优化,并且还会继续优化。
The first optimization avoids extra allocations when []byte keys are used to lookup entries in map[string] collections: m[string(key)].
一个优化是
The second optimization avoids extra allocations in for range clauses where strings are converted to []byte: for i,v := range []byte(str) {...}.
Strings and Index Operator
级别:新手入门级
The index operator on a string returns a byte value, not a character (like it's done in other languages).
package main
import "fmt"
func main() {
x := "text"
fmt.Println(x[0]) //print 116
fmt.Printf("%T",x[0]) //prints uint8
}
If you need to access specific string "characters" (unicode code points/runes) use the for range clause. The official "unicode/utf8" package and the experimental utf8string package (golang.org/x/exp/utf8string) are also useful. The utf8string package includes a convenient At() method. Converting the string to a slice of runes is an option too.
Strings Are Not Always UTF8 Text
级别:新手入门级
String values are not required to be UTF8 text. They can contain arbitrary bytes. The only time strings are UTF8 is when string literals are used. Even then they can include other data using escape sequences.
To know if you have a UTF8 text string use the ValidString() function from the "unicode/utf8" package.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
data1 := "ABC"
fmt.Println(utf8.ValidString(data1)) //prints: true
data2 := "A\xfeC"
fmt.Println(utf8.ValidString(data2)) //prints: false
}
String Length
级别:新手入门级
Let's say you are a python developer and you have the following piece of code:
data = u'♥'
print(len(data)) #prints: 1
When you convert it to a similar Go code snippet you might be surprised.
package main
import "fmt"
func main() {
data := "♥"
fmt.Println(len(data)) //prints: 3
}
The built-in len() function returns the number of bytes instead of the number of characters like it's done for unicode strings in Python.
To get the same results in Go use the RuneCountInString() function from the "unicode/utf8" package.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
data := "♥"
fmt.Println(utf8.RuneCountInString(data)) //prints: 1
Technically the RuneCountInString() function doesn't return the number of characters because a single character may span multiple runes.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
data := "é"
fmt.Println(len(data)) //prints: 3
fmt.Println(utf8.RuneCountInString(data)) //prints: 2
}
Missing Comma In Multi-Line Slice, Array, and Map Literals
级别:新手入门级
错误信息:
package main
func main() {
x := []int{
1,
2 //error
}
_ = x
}
Compile Errors:
/tmp/sandbox367520156/main.go:6: syntax error: need trailing comma before newline in composite literal /tmp/sandbox367520156/main.go:8: non-declaration statement outside function body /tmp/sandbox367520156/main.go:9: syntax error: unexpected }
修正代码:
package main
func main() {
x := []int{
1,
2,
}
x = x
y := []int //no error
y = y
}
You won't get a compiler error if you leave the trailing comma when you collapse the declaration to be on a single line.
log.Fatal and log.Panic Do More Than Log
级别:新手入门级
Logging libraries often provide different log levels. Unlike those logging libraries, the log package in Go does more than log if you call its Fatal() and Panic() functions. When your app calls those functions Go will also terminate your app :-)
package main
import "log"
func main() {
log.Fatalln("Fatal Level: log entry") //app exits here
log.Println("Normal Level: log entry")
}
Built-in Data Structure Operations Are Not Synchronized
级别:新手入门级
Even though Go has a number of features to support concurrency natively, concurrency safe data collections are not one them :-) It's your responsibility to ensure the data collection updates are atomic. Goroutines and channels are the recommended way to implement those atomic operations, but you can also leverage the "sync" package if it makes sense for your application.
Iteration Values For Strings in "range" Clauses
级别:新手入门级
The index value (the first value returned by the "range" operation) is the index of the first byte for the current "character" (unicode code point/rune) returned in the second value. It's not the index for the current "character" like it's done in other languages. Note that an actual character might be represented by multiple runes. Make sure to check out the "norm" package (golang.org/x/text/unicode/norm) if you need to work with characters.
The for range clauses with string variables will try to interpret the data as UTF8 text. For any byte sequences it doesn't understand it will return 0xfffd runes (aka unicode replacement characters) instead of the actual data. If you have arbitrary (non-UTF8 text) data stored in your string variables, make sure to convert them to byte slices to get all stored data as is.
package main
import "fmt"
func main() {
data := "A\xfe\x02\xff\x04"
for _,v := range data {
fmt.Printf("%#x ",v)
}
//prints: 0x41 0xfffd 0x2 0xfffd 0x4 (not ok)
fmt.Println()
for _,v := range []byte(data) {
fmt.Printf("%#x ",v)
}
//prints: 0x41 0xfe 0x2 0xff 0x4 (good)
}
Iterating Through a Map Using a "for range" Clause
级别:新手入门级
This is a gotcha if you expect the items to be in a certain order (e.g., ordered by the key value). Each map iteration will produce different results. The Go runtime tries to go an extra mile randomizing the iteration order, but it doesn't always succeed so you may get several identical map iterations. Don't be surprised to see 5 identical iterations in a row.
package main
import "fmt"
func main() {
m := map[string]int{"one":1,"two":2,"three":3,"four":4}
for k,v := range m {
fmt.Println(k,v)
}
}
And if you use the Go Playground (https://play.golang.org/) you'll always get the same results because it doesn't recompile the code unless you make a change.
Fallthrough Behavior in "switch" Statements
级别:新手入门级
The "case" blocks in "switch" statements break by default. This is different from other languages where the default behavior is to fall through to the next "case" block.
package main
import "fmt"
func main() {
isSpace := func(ch byte) bool {
switch(ch) {
case ' ': //error
case '\t':
return true
}
return false
}
fmt.Println(isSpace('\t')) //prints true (ok)
fmt.Println(isSpace(' ')) //prints false (not ok)
}
You can force the "case" blocks to fall through by using the "fallthrough" statement at the end of each "case" block. You can also rewrite your switch statement to use expression lists in the "case" blocks.
package main
import "fmt"
func main() {
isSpace := func(ch byte) bool {
switch(ch) {
case ' ', '\t':
return true
}
return false
}
fmt.Println(isSpace('\t')) //prints true (ok)
fmt.Println(isSpace(' ')) //prints true (ok)
}
Increments and Decrements
级别:新手入门级
Many languages have increment and decrement operators. Unlike other languages, Go doesn't support the prefix version of the operations. You also can't use these two operators in expressions.
错误信息:
package main
import "fmt"
func main() {
data := []int
i := 0
++i //error
fmt.Println(data[i++]) //error
}
Compile Errors:
/tmp/sandbox101231828/main.go:8: syntax error: unexpected ++ /tmp/sandbox101231828/main.go:9: syntax error: unexpected ++, expecting :
修正代码:
package main
import "fmt"
func main() {
data := []int
i := 0
i++
fmt.Println(data[i])
}
Bitwise NOT Operator
级别:新手入门级
Many languages use ~ as the unary NOT operator (aka bitwise complement), but Go reuses the XOR operator (^) for that.
错误信息:
package main
import "fmt"
func main() {
fmt.Println(~2) //error
}
错误信息:
/tmp/sandbox965529189/main.go:6: the bitwise complement operator is ^
修正代码:
package main
import "fmt"
func main() {
var d uint8 = 2
fmt.Printf("%08b\n",^d)
}
Go still uses ^ as the XOR operator, which may be confusing for some people.
If you want you can represent a unary NOT operation (e.g, NOT 0x02) with a binary XOR operation (e.g., 0x02 XOR 0xff). This could explain why ^ is reused to represent unary NOT operations.
Go also has a special 'AND NOT' bitwise operator (&^), which adds to the NOT operator confusion. It looks like a special feature/hack to support A AND (NOT B) without requiring parentheses.
package main
import "fmt"
func main() {
var a uint8 = 0x82
var b uint8 = 0x02
fmt.Printf("%08b [A]\n",a)
fmt.Printf("%08b [B]\n",b)
fmt.Printf("%08b (NOT B)\n",^b)
fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n",b,0xff,b ^ 0xff)

fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n",a,b,a ^ b)
fmt.Printf("%08b & %08b = %08b [A AND B]\n",a,b,a & b)
fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n",a,b,a &^ b)
fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n",a,b,a & (^b))
}
Operator Precedence Differences
级别:新手入门级
Aside from the "bit clear" operators (&^) Go has a set of standard operators shared by many other languages. The operator precedence is not always the same though.
package main
import "fmt"
func main() {
fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n",0x2 & 0x2 + 0x4)
//prints: 0x2 & 0x2 + 0x4 -> 0x6
//Go: (0x2 & 0x2) + 0x4
//C++: 0x2 & (0x2 + 0x4) -> 0x2
fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n",0x2 + 0x2 << 0x1)
//prints: 0x2 + 0x2 << 0x1 -> 0x6//Go: 0x2 + (0x2 << 0x1)//C++: (0x2 + 0x2) << 0x1 -> 0x8

fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n",0xf | 0x2 ^ 0x2)
//prints: 0xf | 0x2 ^ 0x2 -> 0xd//Go: (0xf | 0x2) ^ 0x2//C++: 0xf | (0x2 ^ 0x2) -> 0xf
}
Unexported Structure Fields Are Not Encoded
级别:新手入门级
The struct fields starting with lowercase letters will not be (json, xml, gob, etc.) encoded, so when you decode the structure you'll end up with zero values in those unexported fields.
package main
import (
"fmt"
"encoding/json"
)
type MyData struct {
One int
two string
}
func main() {
in := MyData
fmt.Printf("%#v\n",in) //prints main.MyData
encoded,_ := json.Marshal(in)
fmt.Println(string(encoded)) //prints {"One":1}

var out MyData
json.Unmarshal(encoded,&out)

fmt.Printf("%#v\n",out) //prints main.MyData
}
App Exits With Active Goroutines
级别:新手入门级
The app will not wait for all your goroutines to complete. This is a common mistake for beginners in general. Everybody starts somewhere, so there's no shame in making rookie mistakes :-)
package main
import (
"fmt"
"time"
)
func main() {
workerCount := 2
for i := 0; i < workerCount; i++ {
go doit(i)
}
time.Sleep(1 * time.Second)
fmt.Println("all done!")
}
func doit(workerId int) {
fmt.Printf("[%v] is running\n",workerId)
time.Sleep(3 * time.Second)
fmt.Printf("[%v] is done\n",workerId)
}
You'll see:
[0] is running
[1] is running
all done!
One of the most common solutions is to use a "WaitGroup" variable. It will allow the main goroutine to wait until all worker goroutines are done. If your app has long running workers with message processing loops you'll also need a way to signal those goroutines that it's time to exit. You can send a "kill" message to each worker. Another option is to close a channel all workers are receiving from. It's a simple way to signal all goroutines at once.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doit(i,done,wg)
}

close(done)
wg.Wait()
fmt.Println("all done!")
}
func doit(workerId int,done <-chan struct{},wg sync.WaitGroup) {
fmt.Printf("[%v] is running\n",workerId)
defer wg.Done()
<- done
fmt.Printf("[%v] is done\n",workerId)
}
If you run this app you'll see:
[0] is running
[0] is done
[1] is running
[1] is done
Looks like the workers are done before the main goroutine exists. Great! However, you'll also see this:
fatal error: all goroutines are asleep - deadlock!
That's not so great :-) What's going on? Why is there a deadlock? The workers exited and they executed wg.Done(). The app should work.
The deadlock happens because each worker gets a copy of the original "WaitGroup" variable. When workers execute wg.Done() it has no effect on the "WaitGroup" variable in the main goroutine.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
wq := make(chan interface{})
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doit(i,wq,done,&wg)
}

for i := 0; i < workerCount; i++ {
wq <- i
}

close(done)
wg.Wait()
fmt.Println("all done!")
}
func doit(workerId int, wq <-chan interface{},done <-chan struct{},wg *sync.WaitGroup) {
fmt.Printf("[%v] is running\n",workerId)
defer wg.Done()
for {
select {
case m := <- wq:
fmt.Printf("[%v] m => %v\n",workerId,m)
case <- done:
fmt.Printf("[%v] is done\n",workerId)
return
}
}
}
Now it works as expected :-)
Sending to an Unbuffered Channel Returns As Soon As the Target Receiver Is Ready
级别:新手入门级
The sender will not be blocked until your message is processed by the receiver. Depending on the machine where you are running the code, the receiver goroutine may or may not have enough time to process the message before the sender continues its execution.
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
for m := range ch {
fmt.Println("processed:",m)
}
}()

ch <- "cmd.1"
ch <- "cmd.2" //won't be processed
}
Sending to an Closed Channel Causes a Panic
级别:新手入门级
Receiving from a closed channel is safe. The ok return value in a receive statement will be set to false indicating that no data was received. If you are receiving from a buffered channel you'll get the buffered data first and once it's empty the ok return value will be false.
Sending data to a closed channel causes a panic. It is a documented behavior, but it's not very intuitive for new Go developers who might expect the send behavior to be similar to the receive behavior.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
for i := 0; i < 3; i++ {
go func(idx int) {
ch <- (idx + 1) * 2
}(i)
}
//get the first result
fmt.Println(<-ch)
close(ch) //not ok (you still have other senders)
//do other worktime.Sleep(2 * time.Second)
}
Depending on your application the fix will be different. It might be a minor code change or it might require a change in your application design. Either way, you'll need to make sure your application doesn't try to send data to a closed channel.
The buggy example can be fixed by using a special cancellation channel to signal the remaining workers that their results are no longer neeeded.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
done := make(chan struct{})
for i := 0; i < 3; i++ {
go func(idx int) {
select {
case ch <- (idx + 1) * 2: fmt.Println(idx,"sent result")
case <- done: fmt.Println(idx,"exiting")
}
}(i)
}
//get first result
fmt.Println("result:",<-ch)
close(done)
//do other worktime.Sleep(3 * time.Second)
}
Using "nil" Channels
级别:新手入门级
Send and receive operations on a nil channel block forver. It's a well documented behavior, but it can be a surprise for new Go developers.
package main
import (
"fmt"
"time"
)
func main() {
var ch chan int
for i := 0; i < 3; i++ {
go func(idx int) {
ch <- (idx + 1) * 2
}(i)
}
//get first result
fmt.Println("result:",<-ch)
//do other worktime.Sleep(2 * time.Second)
}
If you run the code you'll see a runtime error like this: fatal error: all goroutines are asleep - deadlock!
This behavior can be used as a way to dynamically enable and disable case blocks in a select statement.
package main
import "fmt"
import "time"
func main() {
inch := make(chan int)
outch := make(chan int)
go func() {
var in <- chan int = inch
var out chan <- int
var val int
for {
select {
case out <- val:
out = nil
in = inch
case val = <- in:
out = outch
in = nil
}
}
}()

go func() {
for r := range outch {
fmt.Println("result:",r)
}
}()

time.Sleep(0)
inch <- 1
inch <- 2
time.Sleep(3 * time.Second)
}
Methods with Value Receivers Can't Change the Original Value
级别:新手入门级
Method receivers are like regular function arguments. If it's declared to be a value then your function/method gets a copy of your receiver argument. This means making changes to the receiver will not affect the original value unless your receiver is a map or slice variable and you are updating the items in the collection or the fields you are updating in the receiver are pointers.
package main
import "fmt"
type data struct {
num int
key *string
items map[string]bool
}
func (this *data) pmethod() {
this.num = 7
}
func (this data) vmethod() {
this.num = 8
*this.key = "v.key"
this.items["vmethod"] = true
}
func main() {
key := "key.1"
d := data
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
//prints num=1 key=key.1 items=map[]

d.pmethod()
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
//prints num=7 key=key.1 items=map[]

d.vmethod()
fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
//prints num=7 key=v.key items=map[vmethod:true]
}
Closing HTTP Response Body
level: intermediate
When you make requests using the standard http library you get a http response variable. If you don't read the response body you still need to close it. Note that you must do it for empty responses too. It's very easy to forget especially for new Go developers.
Some new Go developers do try to close the response body, but they do it in the wrong place.
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
defer resp.Body.Close()//not ok
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(string(body))
}
This code works for successful requests, but if the http request fails the resp variable might be nil, which will cause a runtime panic.
The most common why to close the response body is by using a defer call after the http response error check.
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()//ok, most of the time :-)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(string(body))
}
Most of the time when your http request fails the resp variable will be nil and the err variable will be non-nil. However, when you get a redirection failure both variables will be non-nil. This means you can still end up with a leak.
You can fix this leak by adding a call to close non-nil response bodies in the http response error handling block. Another option is to use one defer call to close response bodies for all failed and successful requests.
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(string(body))
}
The orignal implementation for resp.Body.Close() also reads and discards the remaining response body data. This ensured that the http connection could be reused for another request if the keepalive http connection behavior is enabled. The latest http client behavior is different. Now it's your responsibility to read and discard the remaining response data. If you don't do it the http connection might be closed instead of being reused. This little gotcha is supposed to be documented in Go 1.5.
If reusing the http connection is important for your application you might need to add something like this at the end of your response processing logic:
_, err = io.Copy(ioutil.Discard, resp.Body)
It might be necessary if you don't read the entire response body right away, which might happen if you are processing json http API response with code like this:
json.NewDecoder(resp.Body).Decode(&data)
Closing HTTP Connections
level: intermediate
Some HTTP servers keep network connections open for a while (based on the HTTP 1.1 spec and the server "keep-alive" configurations). By default, the standard http library will close the network connections only when the target HTTP server asks for it. This means your app may run out of sockets/file descriptors under certain conditions.
You can ask the http library to close the connection after your request is done by setting the Close field in the request variable to true.
Another option is to add a Connection request header and set it to close. The target HTTP server should respond with a Connection: close header too. When the http library sees this response header it will also close the connection.
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
req, err := http.NewRequest("GET","http://golang.org",nil)
if err != nil {
fmt.Println(err)
return
}
req.Close = true
//or do this://req.Header.Add("Connection", "close")

resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}

if err != nil {
fmt.Println(err)
return
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(len(string(body)))
}
You can also disable http connection reuse globally. You'll need to create a custom http transport configuration for it.
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
tr := &http.Transport
client := &http.Client
resp, err := client.Get("http://golang.org")
if resp != nil {
defer resp.Body.Close()
}

if err != nil {
fmt.Println(err)
return
}

fmt.Println(resp.StatusCode)

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}

fmt.Println(len(string(body)))
}
If you send a lot of requests to the same HTTP server it's ok to keep the network connection open. However, if your app sends one or two requests to many different HTTP servers in a short period of time it's a good idea to close the network connections right after your app receives the responses. Increasing the open file limit might be a good idea too. The correct solution depends on your application though.
Comparing Structs, Arrays, Slices, and Maps
level: intermediate
You can use the equality operator, ==, to compare struct variables if each structure field can be compared with the equality operator.
package main
import "fmt"
type data struct {
num int
fp float32
complex complex64
str string
char rune
yes bool
events <-chan string
handler interface{}
ref *byte
raw [10]byte
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",v1 == v2) //prints: v1 == v2: true
}
If any of the struct fields are not comparable then using the equality operator will result in compile time errors. Note that arrays are comparable only if their data items are comparable.
package main
import "fmt"
type data struct {
num int //ok
checks [10]func() bool //not comparable
doit func() bool //not comparable
m map[string] string //not comparable
bytes []byte //not comparable
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",v1 == v2)
}
Go does provide a number of helper functions to compare variables that can't be compared using the comparison operators.
The most generic solution is to use the DeepEqual() function in the reflect package.
package main
import (
"fmt"
"reflect"
)
type data struct {
num int //ok
checks [10]func() bool //not comparable
doit func() bool //not comparable
m map[string] string //not comparable
bytes []byte //not comparable
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2)) //prints: v1 == v2: true
m1 := map[string]string{"one": "a","two": "b"}
m2 := map[string]string{"two": "b", "one": "a"}
fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true

s1 := []int
s2 := []int
fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2)) //prints: s1 == s2: true
}
Aside from being slow (which may or may not be a deal breaker for your application), DeepEqual() also has its own gotchas.
package main
import (
"fmt"
"reflect"
)
func main() {
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2:",reflect.DeepEqual(b1, b2)) //prints: b1 == b2: false
}
DeepEqual() doesn't consider an empty slice to be equal to a "nil" slice. This behavior is different from the behavior you get using the bytes.Equal() function. bytes.Equal() considers "nil" and empty slices to be equal.
package main
import (
"fmt"
"bytes"
)
func main() {
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2:",bytes.Equal(b1, b2)) //prints: b1 == b2: true
}
DeepEqual() isn't always perfect comparing slices.
package main
import (
"fmt"
"reflect"
"encoding/json"
)
func main() {
var str string = "one"
var in interface{} = "one"
fmt.Println("str == in:",str == in,reflect.DeepEqual(str, in))
//prints: str == in: true true
v1 := []string{"one","two"}
v2 := []interface{}{"one","two"}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1, v2))
//prints: v1 == v2: false (not ok)

data := map[string]interface{}{
"code": 200,
"value": []string{"one","two"},
}
encoded, _ := json.Marshal(data)
var decoded map[string]interface{}
json.Unmarshal(encoded, &decoded)
fmt.Println("data == decoded:",reflect.DeepEqual(data, decoded))
//prints: data == decoded: false (not ok)
}
If your byte slices (or strings) contain text data you might be tempted to use ToUpper() or ToLower() from the "bytes" and "strings" packages when you need to compare values in a case insensitive manner (before using ==,bytes.Equal(), or bytes.Compare()). It will work for English text, but it will not work for text in many other languages. strings.EqualFold() and bytes.EqualFold() should be used instead.
If your byte slices contain secrets (e.g., cryptographic hashes, tokens, etc.) that need to be validated against user-provided data, don't use reflect.DeepEqual(), bytes.Equal(), or bytes.Compare() because those functions will make your application vulnerable to timing attacks. To avoid leaking the timing information use the functions from the 'crypto/subtle' package (e.g., subtle.ConstantTimeCompare()).
Recovering From a Panic
level: intermediate
The recover() function can be used to catch/intercept a panic. Calling recover() will do the trick only when it's done in a deferred function.
Incorrect:
package main
import "fmt"
func main() {
recover() //doesn't do anything
panic("not good")
recover() //won't be executed :)
fmt.Println("ok")
}
修正代码:
package main
import "fmt"
func main() {
defer func() {
fmt.Println("recovered:",recover())
}()
panic("not good")
}
The call to recover() works only if it's called directly in your deferred function.
错误信息:
package main
import "fmt"
func doRecover() {
fmt.Println("recovered =>",recover()) //prints: recovered => 
}
func main() {
defer func() {
doRecover() //panic is not recovered
}()
panic("not good")
}
Updating and Referencing Item Values in Slice, Array, and Map "range" Clauses
level: intermediate
The data values generated in the "range" clause are copies of the actual collection elements. They are not references to the original items. This means that updating the values will not change the original data. It also means that taking the address of the values will not give you pointers to the original data.
package main
import "fmt"
func main() {
data := []int
for _,v := range data {
v *= 10 //original item is not changed
}
fmt.Println("data:",data) //prints data: [1 2 3]
}
If you need to update the original collection record value use the index operator to access the data.
package main
import "fmt"
func main() {
data := []int
for i,_ := range data {
data[i] *= 10
}
fmt.Println("data:",data) //prints data: [10 20 30]
}
If your collection holds pointer values then the rules are slightly different. You still need to use the index operator if you want the original record to point to another value, but you can update the data stored at the target location using the second value in the "for range" clause.
package main
import "fmt"
func main() {
data := []*struct {,,}
for _,v := range data {
v.num *= 10
}

fmt.Println(data[0],data[1],data[2]) //prints & & &
}
"Hidden" Data in Slices
level: intermediate
When you reslice a slice, the new slice will reference the array of the original slice. If you forget about this behavior it can lead to unexpected memory usage if your application allocates large temporary slices creating new slices from them to refer to small sections of the original data.
package main
import "fmt"
func get() []byte {
raw := make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 
return raw[:3]
}
func main() {
data := get()
fmt.Println(len(data),cap(data),&data[0]) //prints: 3 10000 
}
To avoid this trap make sure to copy the data you need from the temporary slice (instead of reslicing it).
package main
import "fmt"
func get() []byte {
raw := make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 
res := make([]byte,3)
copy(res,raw[:3])
return res
}
func main() {
data := get()
fmt.Println(len(data),cap(data),&data[0]) //prints: 3 3 
}
Slice Data "Corruption"
level: intermediate
Let's say you need to rewrite a path (stored in a slice). You reslice the path to reference each directory modifying the first folder name and then you combine the names to create a new path.
package main
import (
"fmt"
"bytes"
)
func main() {
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB
dir1 = append(dir1,"suffix"...)
path = bytes.Join([][]byte,[]byte{'/'})

fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB (not ok)

fmt.Println("new path =>",string(path))
}
It didn't work as you expected. Instead of "AAAAsuffix/BBBBBBBBB" you ended up with "AAAAsuffix/uffixBBBB". It happened because both directory slices referenced the same underlying array data from the original path slice. This means that the original path is also modified. Depending on your application this might be a problem too.
This problem can fixed by allocating new slices and copying the data you need. Another option is to use the full slice expression.
package main
import (
"fmt"
"bytes"
)
func main() {
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex:sepIndex] //full slice expression
dir2 := path[sepIndex+1:]
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB
dir1 = append(dir1,"suffix"...)
path = bytes.Join([][]byte,[]byte{'/'})

fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB (ok now)

fmt.Println("new path =>",string(path))
}
The extra parameter in the full slice expression controls the capacity for the new slice. Now appending to that slice will trigger a new buffer allocation instead of overwriting the data in the second slice.
"Stale" Slices
level: intermediate
Multiple slices can reference the same data. This can happen when you create a new slice from an existing slice, for example. If your application relies on this behavior to function properly then you'll need to worry about "stale" slices.
At some point adding data to one of the slices will result in a new array allocation when the original array can't hold any more new data. Now other slices will point to the old array (with old data).
import "fmt"
func main() {
s1 := []int
fmt.Println(len(s1),cap(s1),s1) //prints 3 3 [1 2 3]
s2 := s1[1:]
fmt.Println(len(s2),cap(s2),s2) //prints 2 2 [2 3]

for i := range s2 { s2[i] += 20 }

//still referencing the same array
fmt.Println(s1) //prints [1 22 23]
fmt.Println(s2) //prints [22 23]

s2 = append(s2,4)

for i := range s2 { s2[i] += 10 }

//s1 is now "stale"
fmt.Println(s1) //prints [1 22 23]
fmt.Println(s2) //prints [32 33 14]
}
Type Declarations and Methods
level: intermediate
When you create a type declaration by defining a new type from an existing (non-interface) type, you don't inherit the methods defined for that existing type.
错误信息:
package main
import "sync"
type myMutex sync.Mutex
func main() {
var mtx myMutex
mtx.Lock() //error
mtx.Unlock() //error
}
Compile Errors:
/tmp/sandbox106401185/main.go:9: mtx.Lock undefined (type myMutex has no field or method Lock) /tmp/sandbox106401185/main.go:10: mtx.Unlock undefined (type myMutex has no field or method Unlock)
If you do need the methods from the original type you can define a new struct type embedding the original type as an anonymous field.
修正代码:
package main
import "sync"
type myLocker struct {
sync.Mutex
}
func main() {
var lock myLocker
lock.Lock() //ok
lock.Unlock() //ok
}
Interface type declarations also retain their method sets.
修正代码:
package main
import "sync"
type myLocker sync.Locker
func main() {
var lock myLocker = new(sync.Mutex)
lock.Lock() //ok
lock.Unlock() //ok
}
Breaking Out of "for switch" and "for select" Code Blocks
level: intermediate
A "break" statement without a label only gets you out of the inner switch/select block. If using a "return" statement is not an option then defining a label for the outer loop is the next best thing.
package main
import "fmt"
func main() {
loop:
for {
switch {
case true:
fmt.Println("breaking out...")
break loop
}
}
fmt.Println("out!")
}
A "goto" statement will do the trick too...
Iteration Variables and Closures in "for" Statements
level: intermediate
This is the most common gotcha in Go. The iteration variables in for statements are reused in each iteration. This means that each closure (aka function literal) created in your for loop will reference the same variable (and they'll get that variable's value at the time those goroutines start executing).
Incorrect:
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
go func() {
fmt.Println(v)
}()
}

time.Sleep(3 * time.Second)
//goroutines print: three, three, three
}
The easiest solution (that doesn't require any changes to the goroutine) is to save the current iteration variable value in a local variable inside the for loop block.
修正代码:
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
vcopy := v //
go func() {
fmt.Println(vcopy)
}()
}

time.Sleep(3 * time.Second)
//goroutines print: one, two, three
}
Another solution is to pass the current iteration variable as a parameter to the anonymous goroutine.
修正代码:
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one","two","three"}
for _,v := range data {
go func(in string) {
fmt.Println(in)
}(v)
}

time.Sleep(3 * time.Second)
//goroutines print: one, two, three
}
Here's a slightly more complicated version of the trap.
Incorrect:
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []field{{"one"},{"two"},{"three"}}
for _,v := range data {
go v.print()
}

time.Sleep(3 * time.Second)
//goroutines print: three, three, three
}
修正代码:
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []field{{"one"},{"two"},{"three"}}
for _,v := range data {
v := v
go v.print()
}

time.Sleep(3 * time.Second)
//goroutines print: one, two, three
}
What do you think you'll see when you run this code (and why)?
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []*field{{"one"},{"two"},{"three"}}
for _,v := range data {
go v.print()
}

time.Sleep(3 * time.Second)
}
Deferred Function Call Argument Evaluation
level: intermediate
Arguments for a deferred function call are evaluated when the defer statement is evaluated (not when the function is actually executing).
package main
import "fmt"
func main() {
var i int = 1
defer fmt.Println("result =>",func() int { return i * 2 }())
i++
//prints: result => 2 (not ok if you expected 4)
}
Deferred Function Call Execution
level: intermediate
The deferred calls are executed at the end of the containing function and not at the end of the containing code block. It's an easy mistake to make for new Go developers confusing the deferred code execution rules with the variable scoping rules. It can become a problem if you have a long running function with a for loop that tries to defer resource cleanup calls in each iteration.
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
if len(os.Args) != 2 {
os.Exit(-1)
}
start, err := os.Stat(os.Args[1])
if err != nil || !start.IsDir(){
os.Exit(-1)
}

var targets []string
filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}

if !fi.Mode().IsRegular() {
return nil
}

targets = append(targets,fpath)
return nil
})

for _,target := range targets {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:",target,"error:",err) //prints error: too many open files
break
}
defer f.Close() //will not be closed at the end of this code block
//do something with the file...
}
}
One way to solve the problem is by wrapping the code block in a function.
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
if len(os.Args) != 2 {
os.Exit(-1)
}
start, err := os.Stat(os.Args[1])
if err != nil || !start.IsDir(){
os.Exit(-1)
}

var targets []string
filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}

if !fi.Mode().IsRegular() {
return nil
}

targets = append(targets,fpath)
return nil
})

for _,target := range targets {
func() {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:",target,"error:",err)
return
}
defer f.Close() //ok
//do something with the file...
}()
}
}
Another option is to get rid of the defer statement :-)
Failed Type Assertions
level: intermediate
Failed type assertions return the "zero value" for the target type used in the assertion statement. This can lead to unexpected behavior when it's mixed with variable shadowing.
Incorrect:
package main
import "fmt"
func main() {
var data interface{} = "great"
if data, ok := data.(int); ok {
fmt.Println("[is an int] value =>",data)
} else {
fmt.Println("[not an int] value =>",data)
//prints: [not an int] value => 0 (not "great")
}
}
修正代码:
package main
import "fmt"
func main() {
var data interface{} = "great"
if res, ok := data.(int); ok {
fmt.Println("[is an int] value =>",res)
} else {
fmt.Println("[not an int] value =>",data)
//prints: [not an int] value => great (as expected)
}
}
Blocked Goroutines and Resource Leaks
level: intermediate
Rob Pike talked about a number of fundamental concurrency patterns in his "Go Concurrency Patterns" presentation at Google I/O in 2012. Fetching the first result from a number of targets is one of them.
func First(query string, replicas ...Search) Result {
c := make(chan Result)
searchReplica := func(i int) { c <- replicasi }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
The function starts a goroutines for each search replica. Each goroutine sends its search result to the result channel. The first value from the result channel is returned.
What about the results from the other goroutines? What about the goroutines themselves?
The result channel in the First() function is unbuffered. This means that only the first goroutine returns. All other goroutines are stuck trying to send their results. This means if you have more than one replica each call will leak resources.
To avoid the leaks you need to make sure all goroutines exit. One potential solution is to use a buffered result channel big enough to hold all results.
func First(query string, replicas ...Search) Result {
c := make(chan Result,len(replicas))
searchReplica := func(i int) { c <- replicasi }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
Another potential solution is to use a select statement with a default case and a buffered result channel that can hold one value. The default case ensures that the goroutines don't get stuck even when the result channel can't receive messages.
func First(query string, replicas ...Search) Result {
c := make(chan Result,1)
searchReplica := func(i int) {
select {
case c <- replicasi:
default:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}
You can also use a special cancellation channel to interrupt the workers.
func First(query string, replicas ...Search) Result {
c := make(chan Result)
done := make(chan struct{})
defer close(done)
searchReplica := func(i int) {
select {
case c <- replicasi:
case <- done:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}
Why did the presentation contain these bugs? Rob Pike simply didn't want to comlicate the slides. It makes sense, but it can be a problem for new Go developers who would use the code as is without thinking that it might have problems.
Using Pointer Receiver Methods On Value Instances
level: advanced
It's OK to call a pointer receiver method on a value as long as the value is addressable. In other words, you don't need to have a value receiver version of the method in some cases.
Not every variable is addressable though. Map elements are not addressable. Variables referenced through interfaces are also not addressable.
package main
import "fmt"
type data struct {
name string
}
func (p *data) print() {
fmt.Println("name:",p.name)
}
type printer interface {
print()
}
func main() {
d1 := data{"one"}
d1.print() //ok
var in printer = data{"two"} //errorin.print()

m := map[string]data {"x":data{"three"}}
m["x"].print() //error
}
Compile Errors:
/tmp/sandbox017696142/main.go:21: cannot use data literal (type data) as type printer in assignment: data does not implement printer (print method has pointer receiver)
/tmp/sandbox017696142/main.go:25: cannot call pointer method on m["x"] /tmp/sandbox017696142/main.go:25: cannot take the address of m["x"]
Updating Map Value Fields
level: advanced
If you have a map of struct values you can't update individual struct fields.
错误信息:
package main
type data struct {
name string
}
func main() {
m := map[string]data {"x":{"one"}}
m["x"].name = "two" //error
}
错误信息:
/tmp/sandbox380452744/main.go:9: cannot assign to m["x"].name
It doesn't work because map elements are not addressable.
What can be extra confusing for new Go devs is the fact that slice elements are addressable.
package main
import "fmt"
type data struct {
name string
}
func main() {
s := []data {{"one"}}
s[0].name = "two" //ok
fmt.Println(s) //prints: []
}
Note that a while ago it was possible to update map element fields in one of the Go compilers (gccgo), but that behavior was quickly fixed :-) It was also considered as a potential feature for Go 1.3. It wasn't important enough to support at that point in time, so it's still on the todo list.
The first work around is to use a temporary variable.
package main
import "fmt"
type data struct {
name string
}
func main() {
m := map[string]data {"x":{"one"}}
r := m["x"]
r.name = "two"
m["x"] = r
fmt.Printf("%v",m) //prints: map[x:]
}
Another workaround is to use a map of pointers.
package main
import "fmt"
type data struct {
name string
}
func main() {
m := map[string]*data {"x":{"one"}}
m["x"].name = "two" //ok
fmt.Println(m["x"]) //prints: &
}
By the way, what happens when you run this code?
package main
type data struct {
name string
}
func main() {
m := map[string]*data {"x":{"one"}}
m["z"].name = "what?" //???
}
"nil" Interfaces and "nil" Interfaces Values
level: advanced
This is the second most common gotcha in Go because interfaces are not pointers even though they may look like pointers. Interface variables will be "nil" only when their type and value fields are "nil".
The interface type and value fields are populated based on the type and value of the variable used to create the corresponding interface variable. This can lead to unexpected behavior when you are trying to check if an interface variable equals to "nil".
package main
import "fmt"
func main() {
var data *byte
var in interface{}
fmt.Println(data,data == nil) //prints: <nil> true
fmt.Println(in,in == nil) //prints: <nil> true

in = data
fmt.Println(in,in == nil) //prints: <nil> false
//'data' is 'nil', but 'in' is not 'nil'
}
Watch out for this trap when you have a function that returns interfaces.
Incorrect:
package main
import "fmt"
func main() {
doit := func(arg int) interface{} {
var result *struct{} = nil
if(arg > 0) {
result = &struct{}{}
}


jvm的内存区域还包括一个直接内存,直接内存默认和堆内存一样大,垃圾收集器无法回收直接内存,那直接内存的垃圾回收工作如何做呢?需要像C语言一样由程序员回收吗?后面给出答案。
一.直接内存的内存分配API和垃圾回收API
   jdk中的直接内存的内存分配的入口点:
   Unsafe.allocateMemory(size)
   该方法类似于C语言中的malloc方法,将在直接内存中分配一段大小为size的内存并返回内存的起始地址,如果内存不够,将抛出OOM。
   jdk中的直接内存的内存释放方法时:
    Unsafe.freeMemory(address);
   该方法类似于C语言中的free方法,将回收起始地址为address的一段内存。
   通过allocateMemory方法分配的内存不受jvm管理,无法被垃圾收集器进行回收,因此必须要使用freeMemory方法进行回收,否则会造成直接内存的OOM,而且还无法打印dump,很难定位问题!
   就像C语言中的malloc和free是一对一样。哈哈!

二.DirectByteBufferByteBuffer.allocateDirect方法
   Unsafe类不通过修改编译器和反射调用,是无法直接使用的,也不推荐使用,所以我们往往接触的直接内存只有一种情况,那就是通过ByteBuffer.allocateDirect(size)方法得到的含有直接内存的DirectByteBuffer对象。
   看看DirectBuffer是怎么利用Uunsafe.allocateMemory方法和Unsafe.freeMeomory方法来分配和回收直接内存的。
   源码如下:
   DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);

        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);
        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

      从这儿可以看出Unsafe的确是显示调用unsafe.allocateMemory方法来直接分配内存的。
      那在哪儿进行内存回收的工作呢?
      如下:
      cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
      private final Cleaner cleaner;
       private static class Deallocator
        implements Runnable
    {
        private static Unsafe unsafe = Unsafe.getUnsafe();
        private long address;
        private long size;
        private int capacity;
        private Deallocator(long address, long size, int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }
        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
    }
   从这儿可以看出内存回收工作是由Dellocator和Cleaner来做的,Dellocator实现了Runable,里面直接调用了unsafe.freeMemory方法来回收DirectBuffer中的直接内存,而Dellocator的对象又被传递了给了Cleaner,Cleaner是一个Reference子类,当DirectBuffer只有Cleaner等非强引用可达时,GC标记后会由一个全局线程来调用Cleaner的Clean方法,而Clean方法又回调Dellocator来进行直接内存的释放工作。
至于JVM如何调用Cleaner,那是在垃圾收集时候,由GC标记后交给ReferenceHandler线程来处理的,GC标记需要参考jdk底层代码,后者可以在Reference类中找到调用逻辑。
Bits.unreserveMemory(size, capacity);还有一些进一步的处理动作,这儿不做总结。

三.直接内存一些总结

使用堆外内存的原因

  • 对垃圾回收停顿的改善
因为full gc 意味着彻底回收,彻底回收时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,这意味着一个重要的事实——这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。
  • 在某些场景下可以提升程序I/O操纵的性能。少去了将数据从堆内内存拷贝到堆外内存的步骤。

什么情况下使用堆外内存

  • 堆外内存适用于生命周期中等或较长的对象。( 如果是生命周期较短的对象,在YGC的时候就被回收了,就不存在大内存且生命周期较长的对象在FGC对应用造成的性能影响 )。
  • 直接的文件拷贝操作,或者I/O操作。直接使用堆外内存就能少去内存从用户内存拷贝到系统内存的操作,因为I/O操作是系统内核内存和设备间的通信,而不是通过程序直接和外设通信的。
  • 同时,还可以使用 池+堆外内存 的组合方式,来对生命周期较短,但涉及到I/O操作的对象进行堆外内存的再使用。( Netty中就使用了该方式 )

堆外内存 VS 内存池

  • 内存池:主要用于两类对象:①生命周期较短,且结构简单的对象,在内存池中重复利用这些对象能增加CPU缓存的命中率,从而提高性能;②加载含有大量重复对象的大片数据,此时使用内存池能减少垃圾回收的时间。
  • 堆外内存:它和内存池一样,也能缩短垃圾回收时间,但是它适用的对象和内存池完全相反。内存池往往适用于生命期较短的可变对象,而生命期中等或较长的对象,正是堆外内存要解决的。

堆外内存的特点

  • 对于大内存有良好的伸缩性
  • 对垃圾回收停顿的改善可以明显感觉到
  • 在进程间可以共享,减少虚拟机间的复制

堆外内存的一些问题

  • 堆外内存回收问题,以及堆外内存的泄漏问题。这个在上面的源码解析已经提到了
  • 堆外内存的数据结构问题:堆外内存最大的问题就是你的数据结构变得不那么直观,如果数据结构比较复杂,就要对它进行串行化(serialization),而串行化本身也会影响性能。另一个问题是由于你可以使用更大的内存,你可能开始担心虚拟内存(即硬盘)的速度对你的影响了。


原文地址:http://blog.csdn.net/zjf280441589/article/details/54406665
问题: 大部分主流互联网企业线上Server JVM选用了CMS收集器(如Taobao、LinkedIn、Vdian), 虽然CMS可与用户线程并发GC以降低STW时间, 但它也并非十分完美, 尤其是当出现Concurrent Mode Failure由并行GC转入串行时, 将导致非常长时间的Stop The World(详细可参考JVM初探- 内存分配、GC原理与垃圾收集器).
解决: 由GCIH可以联想到: 将长期存活的对象(如Local Cache)移入堆外内存(off-heap, 又名直接内存/direct-memory), 从而减少CMS管理的对象数量, 以降低Full GC的次数和频率, 达到提高系统响应速度的目的.

引入

这个idea最初来源于TaobaoJVM对OpenJDK定制开发的GCIH部分(详见撒迦的分享-JVM定制改进@淘宝), 其中GCIH就是将CMS Old Heap区的一部分划分出来, 这部分内存虽然还在堆内, 但已不被GC所管理.将长生命周期Java对象放在Java堆外, GC不能管理GCIH内Java对象(GC Invisible Heap)
(图片来源: JVM@Taobao PPT)
这样做有两方面的好处: 
减少GC管理内存: 
由于GCIH会从Old区“切出”一块, 因此导致GC管理区域变小, 可以明显降低GC工作量, 提高GC效率, 降低Full GC STW时间(且由于这部分内存仍属于堆, 因此其访问方式/速度不变- 不必付出序列化/反序列化的开销).
GCIH内容进程间共享: 
由于这部分区域不再是JVM运行时数据的一部分, 因此GCIH内的对象可供对个JVM实例所共享(如一台Server跑多个MR-Job可共享同一份Cache数据), 这样一台Server也就可以跑更多的VM实例.
(实际测试数据/图示可下载撒迦分享PPT).
但是大部分的互联公司不能像阿里这样可以有专门的工程师针对自己的业务特点定制JVM, 因此我们只能”眼馋”GCIH带来的性能提升却无法”享用”. 但通用的JVM开放了接口可直接向操作系统申请堆外内存(ByteBuffer or Unsafe), 而这部分内存也是GC所顾及不到的, 因此我们可用JVM堆外内存来模拟GCIH的功能(但相比GCIH不足的是需要付出serialize/deserialize的开销).

JVM堆外内存

JVM初探 -JVM内存模型一文中介绍的Java运行时数据区域中是找不到堆外内存区域的: 
 
因为它并不是JVM运行时数据区的一部分, 也不是Java虚拟机规范中定义的内存区域, 这部分内存区域直接被操作系统管理. 
在JDK 1.4以前, 对这部分内存访问没有光明正大的做法: 只能通过反射拿到Unsafe类, 然后调用allocateMemory()/freeMemory()来申请/释放这块内存. 1.4开始新加入了NIO, 它引入了一种基于Channel与Buffer的I/O方式, 可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作, ByteBuffer提供了如下常用方法来跟堆外内存打交道:
API
描述
static ByteBuffer allocateDirect(int capacity)
Allocates a new direct byte buffer.
ByteBuffer put(byte b)
Relative put method (optional operation).
ByteBuffer put(byte[] src)
Relative bulk put method (optional operation).
ByteBuffer putXxx(Xxx value)
Relative put method for writing a Char/Double/Float/Int/Long/Short value (optional operation).
ByteBuffer get(byte[] dst)
Relative bulk get method.
Xxx getXxx()
Relative get method for reading a Char/Double/Float/Int/Long/Short value.
XxxBuffer asXxxBuffer()
Creates a view of this byte buffer as a Char/Double/Float/Int/Long/Short buffer.
ByteBuffer asReadOnlyBuffer()
Creates a new, read-only byte buffer that shares this buffer’s content.
boolean isDirect()
Tells whether or not this byte buffer is direct.
ByteBuffer duplicate()
Creates a new byte buffer that shares this buffer’s content.
下面我们就用通用的JDK API来使用堆外内存来实现一个local cache.

示例1.: 使用JDK API实现堆外Cache

注: 主要逻辑都集中在方法invoke()内, 而AbstractAppInvoker是一个自定义的性能测试框架, 在后面会有详细的介绍.
/**
* @author jifang
* @since 2016/12/31 下午6:05.
*/public class DirectByteBufferApp extends AbstractAppInvoker {

@Test
@Override
public void invoke(Object... param) {
Map<String, FeedDO> map = createInHeapMap(SIZE);

// move in off-heap
byte[] bytes = serializer.serialize(map);
ByteBuffer buffer = ByteBuffer.allocateDirect(bytes.length);
buffer.put(bytes);
buffer.flip();

// for gc
map = null;
bytes = null;
System.out.println("write down");
// move out from off-heap
byte[] offHeapBytes = new byte[buffer.limit()];
buffer.get(offHeapBytes);
Map<String, FeedDO> deserMap = serializer.deserialize(offHeapBytes);
for (int i = 0; i < SIZE; ++i) {
String key = "key-" + i;
FeedDO feedDO = deserMap.get(key);
checkValid(feedDO);

if (i % 10000 == 0) {
System.out.println("read " + i);
}
}

free(buffer);
}

private Map<String, FeedDO> createInHeapMap(int size) {
long createTime = System.currentTimeMillis();

Map<String, FeedDO> map = new ConcurrentHashMap<>(size);
for (int i = 0; i < size; ++i) {
String key = "key-" + i;
FeedDO value = createFeed(i, key, createTime);
map.put(key, value);
}

return map;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
由JDK提供的堆外内存访问API只能申请到一个类似一维数组的ByteBuffer, JDK并未提供基于堆外内存的实用数据结构实现(如堆外的MapSet), 因此想要实现Cache的功能只能在write()时先将数据put()到一个堆内的HashMap, 然后再将整个Map序列化后MoveInDirectMemory, 取缓存则反之. 由于需要在堆内申请HashMap, 因此可能会导致多次Full GC. 这种方式虽然可以使用堆外内存, 但性能不高、无法发挥堆外内存的优势. 
幸运的是开源界的前辈开发了诸如EhcacheMapDBChronicle Map等一系列优秀的堆外内存框架, 使我们可以在使用简洁API访问堆外内存的同时又不损耗额外的性能.
其中又以Ehcache最为强大, 其提供了in-heap、off-heap、on-disk、cluster四级缓存, 且Ehcache企业级产品(BigMemory Max / BigMemory Go)实现的BigMemory也是Java堆外内存领域的先驱.

示例2: MapDB API实现堆外Cache

public class MapDBApp extends AbstractAppInvoker {

private static HTreeMap<String, FeedDO> mapDBCache;

static {
mapDBCache = DBMaker.hashMapSegmentedMemoryDirect()
.expireMaxSize(SIZE)
.make();
}

@Test
@Override
public void invoke(Object... param) {

for (int i = 0; i < SIZE; ++i) {
String key = "key-" + i;
FeedDO feed = createFeed(i, key, System.currentTimeMillis());

mapDBCache.put(key, feed);
}

System.out.println("write down");
for (int i = 0; i < SIZE; ++i) {
String key = "key-" + i;
FeedDO feedDO = mapDBCache.get(key);
checkValid(feedDO);

if (i % 10000 == 0) {
System.out.println("read " + i);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

结果 & 分析

DirectByteBufferApp
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 5.22 78.57 59.85 19 2.902 13 7.251 10.153
1
2
the last one jstat of MapDBApp
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.03 8.02 0.38 44.46 171 0.238 0 0.000 0.238
1
2
运行DirectByteBufferApp.invoke()会发现有看到很多Full GC的产生, 这是因为HashMap需要一个很大的连续数组, Old区很快就会被占满, 因此也就导致频繁Full GC的产生. 
而运行MapDBApp.invoke()可以看到有一个DirectMemory持续增长的过程, 但FullGC却一次都没有了. 

实验: 使用堆外内存减少Full GC

实验环境

java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
1
2
3
VM Options
-Xmx512M-XX:MaxDirectMemorySize=512M
-XX:+PrintGC-XX:+UseConcMarkSweepGC-XX:+CMSClassUnloadingEnabled-XX:CMSInitiatingOccupancyFraction=80-XX:+UseCMSInitiatingOccupancyOnly
1
2
3
4
5
6
7
实验数据 
170W条动态(FeedDO).

实验代码

第1组: in-heap、affect by GC、no serialize

ConcurrentHashMapApp
public class ConcurrentHashMapApp extends AbstractAppInvoker {

private static final Map<String, FeedDO> cache = new ConcurrentHashMap<>();

@Test
@Override
public void invoke(Object... param) {

// write
for (int i = 0; i < SIZE; ++i) {
String key = String.format("key_%s", i);
FeedDO feedDO = createFeed(i, key, System.currentTimeMillis());
cache.put(key, feedDO);
}

System.out.println("write down");
// read
for (int i = 0; i < SIZE; ++i) {
String key = String.format("key_%s", i);
FeedDO feedDO = cache.get(key);
checkValid(feedDO);

if (i % 10000 == 0) {
System.out.println("read " + i);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
GuavaCacheApp类似, 详细代码可参考完整项目.

第2组: off-heap、not affect by GC、need serialize

EhcacheApp
public class EhcacheApp extends AbstractAppInvoker {

private static Cache<String, FeedDO> cache;

static {
ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(1000, EntryUnit.ENTRIES)
.offheap(480, MemoryUnit.MB)
.build();

CacheConfiguration<String, FeedDO> configuration = CacheConfigurationBuilder
.newCacheConfigurationBuilder(String.class, FeedDO.class, resourcePools)
.build();

cache = CacheManagerBuilder.newCacheManagerBuilder()
.withCache("cacher", configuration)
.build(true)
.getCache("cacher", String.class, FeedDO.class);

}

@Test
@Override
public void invoke(Object... param) {
for (int i = 0; i < SIZE; ++i) {
String key = String.format("key_%s", i);
FeedDO feedDO = createFeed(i, key, System.currentTimeMillis());
cache.put(key, feedDO);
}

System.out.println("write down");
// read
for (int i = 0; i < SIZE; ++i) {
String key = String.format("key_%s", i);
Object o = cache.get(key);
checkValid(o);

if (i % 10000 == 0) {
System.out.println("read " + i);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
MapDBApp与前同.

第3组: off-process、not affect by GC、serialize、affect by process communication

LocalRedisApp
public class LocalRedisApp extends AbstractAppInvoker {

private static final Jedis cache = new Jedis("localhost", 6379);

private static final IObjectSerializer serializer = new Hessian2Serializer();

@Test
@Override
public void invoke(Object... param) {
// write
for (int i = 0; i < SIZE; ++i) {
String key = String.format("key_%s", i);
FeedDO feedDO = createFeed(i, key, System.currentTimeMillis());

byte[] value = serializer.serialize(feedDO);
cache.set(key.getBytes(), value);

if (i % 10000 == 0) {
System.out.println("write " + i);
}
}

System.out.println("write down");
// read
for (int i = 0; i < SIZE; ++i) {
String key = String.format("key_%s", i);
byte[] value = cache.get(key.getBytes());
FeedDO feedDO = serializer.deserialize(value);
checkValid(feedDO);

if (i % 10000 == 0) {
System.out.println("read " + i);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
RemoteRedisApp类似, 详细代码可参考下面完整项目.

实验结果

*
ConcurrentMap
Guava
TTC
32166ms/32s
47520ms/47s
Minor C/T
31/1.522
29/1.312
Full C/T
24/23.212
36/41.751
MapDB
Ehcache
TTC
40272ms/40s
30814ms/31s
Minor C/T
511/0.557
297/0.430
Full C/T
0/0.000
0/0.000
LocalRedis
NetworkRedis
TTC
176382ms/176s
1h+
Minor C/T
421/0.415
-
Full C/T
0/0.000
-
备注: 
- TTC: Total Time Cost 总共耗时 
- C/T: Count/Time 次数/耗时(seconds)

结果分析

对比前面几组数据, 可以有如下总结:
将长生命周期的大对象(如cache)移出heap可大幅度降低Full GC次数与耗时;
使用off-heap存储对象需要付出serialize/deserialize成本;
将cache放入分布式缓存需要付出进程间通信/网络通信的成本(UNIX Domain/TCP IP)
附: 
off-heap的Ehcache能够跑出比in-heap的HashMap/Guava更好的成绩确实是我始料未及的O(∩_∩)O~, 但确实这些数据和堆内存的搭配导致in-heap的Full GC太多了, 当heap堆开大之后就肯定不是这个结果了. 因此在使用堆外内存降低Full GC前, 可以先考虑是否可以将heap开的更大.

附: 性能测试框架

在main函数启动时, 扫描com.vdian.se.apps包下的所有继承了AbstractAppInvoker的类, 然后使用Javassist为每个类生成一个代理对象: 当invoke()方法执行时首先检查他是否标注了@Test注解(在此, 我们借用junit定义好了的注解), 并在执行的前后记录方法执行耗时, 并最终对比每个实现类耗时统计.
依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-proxy</artifactId>
<version>${commons.proxy.version}</version></dependency><dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>${javassist.version}</version></dependency><dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>${hessian.version}</version></dependency><dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version></dependency><dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version></dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

启动类: OffHeapStarter

/**
* @author jifang
* @since 2017/1/1 上午10:47.
*/public class OffHeapStarter {

private static final Map<String, Long> STATISTICS_MAP = new HashMap<>();

public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException {
Set<Class<?>> classes = PackageScanUtil.scanPackage("com.vdian.se.apps");
for (Class<?> clazz : classes) {
AbstractAppInvoker invoker = createProxyInvoker(clazz.newInstance());
invoker.invoke();

//System.gc();
}

System.out.println("********************* statistics **********************");
for (Map.Entry<String, Long> entry : STATISTICS_MAP.entrySet()) {
System.out.println("method [" + entry.getKey() + "] total cost [" + entry.getValue() + "]ms");
}
}

private static AbstractAppInvoker createProxyInvoker(Object invoker) {
ProxyFactory factory = new JavassistProxyFactory();
Class<?> superclass = invoker.getClass().getSuperclass();
Object proxy = factory
.createInterceptorProxy(invoker, new ProfileInterceptor(), new Class[]{superclass});
return (AbstractAppInvoker) proxy;
}

private static class ProfileInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
Class<?> clazz = invocation.getProxy().getClass();
Method method = clazz.getMethod(invocation.getMethod().getName(), Object[].class);

Object result = null;
if (method.isAnnotationPresent(Test.class)
&& method.getName().equals("invoke")) {

String methodName = String.format("%s.%s", clazz.getSimpleName(), method.getName());
System.out.println("method [" + methodName + "] start invoke");

long start = System.currentTimeMillis();
result = invocation.proceed();
long cost = System.currentTimeMillis() - start;

System.out.println("method [" + methodName + "] total cost [" + cost + "]ms");

STATISTICS_MAP.put(methodName, cost);
}

return result;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
包扫描工具: PackageScanUtil
public class PackageScanUtil {

private static final String CLASS_SUFFIX = ".class";

private static final String FILE_PROTOCOL = "file";

public static Set<Class<?>> scanPackage(String packageName) throws IOException {

Set<Class<?>> classes = new HashSet<>();
String packageDir = packageName.replace('.', '/');
Enumeration<URL> packageResources = Thread.currentThread().getContextClassLoader().getResources(packageDir);
while (packageResources.hasMoreElements()) {
URL packageResource = packageResources.nextElement();

String protocol = packageResource.getProtocol();
// 只扫描项目内class
if (FILE_PROTOCOL.equals(protocol)) {
String packageDirPath = URLDecoder.decode(packageResource.getPath(), "UTF-8");
scanProjectPackage(packageName, packageDirPath, classes);
}
}

return classes;
}

private static void scanProjectPackage(String packageName, String packageDirPath, Set<Class<?>> classes) {

File packageDirFile = new File(packageDirPath);
if (packageDirFile.exists() && packageDirFile.isDirectory()) {

File[] subFiles = packageDirFile.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory() || pathname.getName().endsWith(CLASS_SUFFIX);
}
});

for (File subFile : subFiles) {
if (!subFile.isDirectory()) {
String className = trimClassSuffix(subFile.getName());
String classNameWithPackage = packageName + "." + className;

Class<?> clazz = null;
try {
clazz = Class.forName(classNameWithPackage);
} catch (ClassNotFoundException e) {
// ignore
}
assert clazz != null;

Class<?> superclass = clazz.getSuperclass();
if (superclass == AbstractAppInvoker.class) {
classes.add(clazz);
}
}
}
}
}

// trim .class suffix
private static String trimClassSuffix(String classNameWithSuffix) {
int endIndex = classNameWithSuffix.length() - CLASS_SUFFIX.length();
return classNameWithSuffix.substring(0, endIndex);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
注: 在此仅扫描项目目录下的单层目录的class文件, 功能更强大的包扫描工具可参考Spring源代码或Touch源代码中的PackageScanUtil.

AppInvoker基类: AbstractAppInvoker

提供通用测试参数 & 工具函数.
public abstract class AbstractAppInvoker {

protected static final int SIZE = 170_0000;

protected static final IObjectSerializer serializer = new Hessian2Serializer();

protected static FeedDO createFeed(long id, String userId, long createTime) {

return new FeedDO(id, userId, (int) id, userId + "_" + id, createTime);
}

protected static void free(ByteBuffer byteBuffer) {
if (byteBuffer.isDirect()) {
((DirectBuffer) byteBuffer).cleaner().clean();
}
}

protected static void checkValid(Object obj) {
if (obj == null) {
throw new RuntimeException("cache invalid");
}
}

protected static void sleep(int time, String beforeMsg) {
if (!Strings.isNullOrEmpty(beforeMsg)) {
System.out.println(beforeMsg);
}

try {
Thread.sleep(time);
} catch (InterruptedException ignored) {
// no op
}
}


/**
* 供子类继承 & 外界调用
*
* @param param
*/
public abstract void invoke(Object... param);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

序列化/反序列化接口与实现

public interface IObjectSerializer {

<T> byte[] serialize(T obj);

<T> T deserialize(byte[] bytes);
}
1
2
3
4
5
6
public class Hessian2Serializer implements IObjectSerializer {

private static final Logger LOGGER = LoggerFactory.getLogger(Hessian2Serializer.class);

@Override
public <T> byte[] serialize(T obj) {
if (obj != null) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {

Hessian2Output out = new Hessian2Output(os);
out.writeObject(obj);
out.close();
return os.toByteArray();

} catch (IOException e) {
LOGGER.error("Hessian serialize error ", e);
throw new CacherException(e);
}
}
return null;
}

@SuppressWarnings("unchecked")
@Override
public <T> T deserialize(byte[] bytes) {
if (bytes != null) {
try (ByteArrayInputStream is = new ByteArrayInputStream(bytes)) {

Hessian2Input in = new Hessian2Input(is);
T obj = (T) in.readObject();
in.close();

return obj;

} catch (IOException e) {
LOGGER.error("Hessian deserialize error ", e);
throw new CacherException(e);
}
}
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
完整项目地址: https://github.com/feiqing/off-heap-tester.git.

GC统计工具

#!/bin/bash

pid=`jps | grep $1 | awk '{print $1}'`
jstat -gcutil ${pid} 400 10000
1
2
3
4
使用 
sh jstat-uti.sh ${u-main-class}

附加: 为什么在实验中in-heap cache的Minor GC那么少? 
现在我还不能给出一个确切地分析答案, 有的同学说是因为CMS Full GC会连带一次Minor GC, 而用jstat会直接计入Full GC, 但查看详细的GC日志也并未发现什么端倪. 希望有了解的同学可以在下面评论区可以给我留言, 再次先感谢了O(∩_∩)O~.

by 攻城师@翡青 
Email: feiqing.zjf@gmail.com
博客: 攻城师-翡青 - http://blog.csdn.net/zjf280441589
微博: 攻城师-翡青 - http://weibo.com/u/3319050953



finalizer就是我们重写finalize方法后,垃圾回收时标记的载体,可以用于资源释放和对象自我拯救(只能)。
Cleaner实际上是一个Reference的子类,可以注册到一个对象上(registe方法),注册以后,Cleaner的referent引用就会指向该对象,所以当该对象只有几个特殊引用可达(软、弱、虚、cleaner)且经过垃圾收集器的GC标记,ReferenceHandler线程将调用其Clean方法,而Clean方法实际上是一个调用一个回调对象(runnable)。典型用例:DirectBuffer内部的直接内存回收。

见Cleaner文档:
Module java.base
Package java.lang.ref

Class Cleaner

java.lang.Objectjava.lang.ref.Cleaner


个人理解
PhantomReference是Reference的一个子类,Cleaner是PhantomReference的一个子类。
我们都知道虚引用不参与对象的引用分析当中,无法阻止对象的垃圾回收,即一个对象的所有强引用都已经没有了,只剩虚引用,那么这个对象还是会被垃圾回收。
Cleaner继承自PhantomReference,它本质上仍然是一个Reference。所以它的处理逻辑与WeakReference,SoftReference十分相似,仍然是由GC标记,然后由Reference Handler线程来处理。
Cleaner本身不带有清理逻辑,所有的逻辑都封装在thunk(Runnable字段对象)中,因此thunk是怎么实现的才是最关键的,Reference Handler线程会自动调用Cleaner的clean方法(看上面)。
总结来说:Cleaner是一个Reference子类,其自身可以携带一个Runnable对象来在被ReferenceHandler线程处理时进行回调,从而可以实现一些资源释放的操作。
原文
地址:https://zhuanlan.zhihu.com/p/29454205
在讲DirectBuffer的时候,圈里有同学问我,关于DirectBuffer如何回收的问题。我当时回答说别急,等我讲完GC再来讲这个问题。课程进行到现在,终于可以讲一下了。
其实,在讲DirectBuffer的时候,就已经贴过这一段代码了,但是当时没有做为重点去讲解。今天我们再返回来看一下DirectByteBuffer的构造函数:
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);

long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 这个cleaner就是DirectBuffer回收的最关键部分。
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
前边的部分大家再复习一下,今天的重点是倒数第三行的那个cleaner到底是什么鬼。我们点开Cleaner的定义看一下:
public class Cleaner
extends PhantomReference<Object>
{

// Dummy reference queue, needed because the PhantomReference constructor
// insists that we pass a queue. Nothing will ever be placed on this queue
// since the reference handler invokes cleaners explicitly.
// 就像英文注释所说的,这货没啥卵用。后面我会讲到的。
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

// Doubly-linked list of live cleaners, which prevents the cleaners
// themselves from being GC'd before their referents
// 所有的cleaner都会被加到一个双向链表中去,这样做是为了保证在referent被回收之前
// 这些Cleaner都是存活的。
static private Cleaner first = null;

private Cleaner
next = null,
prev = null;

// 构造的时候把自己加到双向链表中去
private static synchronized Cleaner add(Cleaner cl) {
if (first != null) {
cl.next = first;
first.prev = cl;
}
first = cl;
return cl;
}

// clean方法会调用remove把当前的cleaner从链表中删除。
private static synchronized boolean remove(Cleaner cl) {
// If already removed, do nothing
if (cl.next == cl)
return false;

// Update list
if (first == cl) {
if (cl.next != null)
first = cl.next;
else
first = cl.prev;
}
if (cl.next != null)
cl.next.prev = cl.prev;
if (cl.prev != null)
cl.prev.next = cl.next;

// Indicate removal by pointing the cleaner to itself
cl.next = cl;
cl.prev = cl;
return true;
}

// 用户自定义的一个Runnable对象,
private final Runnable thunk;

// 私有有构造函数,保证了用户无法单独地使用new来创建Cleaner。
private Cleaner(Object referent, Runnable thunk) {
super(referent, dummyQueue);
this.thunk = thunk;
}

/**
* 所有的Cleaner都必须通过create方法进行创建。
*/
public static Cleaner create(Object ob, Runnable thunk) {
if (thunk == null)
return null;
return add(new Cleaner(ob, thunk));
}

/**
* 这个方法会被Reference Handler线程调用,来清理资源。
*/
public void clean() {
if (!remove(this))
return;
try {
thunk.run();
} catch (final Throwable x) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null)
new Error("Cleaner terminated abnormally", x)
.printStackTrace();
System.exit(1);
return null;
}});
}
}
}
这段代码的关键注释我已经加上了。然后我再把要注意的点单独强调一下。
  1. Cleaner继承自PhantomReference,它本质上仍然是一个Reference。所以它的处理方法与WeakReference,SoftReference十分相似。仍然是由GC标记,Reference Handler线程处理的。
  2. Cleaner本身不带有清理逻辑,所有的逻辑都封装在thunk中,因此thunk是怎么实现的才是最关键的。
  3. Cleaner中的next和prev是private的,一定要记住这一点,不要和Reference的成员变量next 混淆了。它们不是一回事。这个next, prev是双向链表,而Reference的next则是由JVM维护的。
OK,关于Reference Handler的代码,请看这篇文章:WeakReference
注意看啊,Reference的定义里新启的那个线程,它的run方法会专门判断从pending链表上取出来的那个对象是不是Cleaner,如果是就会调用它的clean方法。所以我们知道了,Cleaner的clean方法是由Reference Handler线程调用的
好,到此为止,我们再回顾一下整个全景图:
首先,DirectBuffer buf 如果已经没有强引用了,那么JVM就会发现只有一个Cleaner还在引用着这个buf,那么就会把与此buf相关的那个cleaner放到一个名为pending的链表里。这个链表是通过Reference.discovered域连接在一起的。
接着,Reference Handler这个线程会不断地从这个pending链表上取出新的对象来。它可能是WeakReference,也可能是SoftReference,当然也可能是PhantomReference和Cleaner。
最后,如果是Cleaner,那就直接调用Cleaner的clean方法,然后就结束了。其他的情况下,要交给这个对象所关联的queue,以便于后续的处理。
关于Cleaner和PhantomReference就讲这么多了,下节课讲finalize,还会再补充一点cleaner和Finalizer的对比。如果有同学还想了解更多,就评论或者私信问我都行。当然,关于DirectBuffer,还有一点要补充,那就是Cleaner的第二个构造参数是一个Runnable类型的thunk:
private static class Deallocator
implements Runnable
{

private static Unsafe unsafe = Unsafe.getUnsafe();

private long address;
private long size;
private int capacity;

private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}

public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
嗯。在它的run方法里,真正地调用了freeMemory来释放内存。
DirectBuffer释放的全部差不多就都在这里了。当然,说了这么多,其实DirectBuffer的释放时机还是不确定的。首先,得发生GC,其次,Reference Handler得调度到,然后处理到你的cleaner才行,还有一条,如果你自己要实现一个cleaner,千万千万不要在run方法里写一些执行时间很长,或者会阻塞线程的逻辑的。会把Reference Handler跑死的。
上一节课:WeakReference vs. SoftReference
下一节课:finalize方法


Reference引用的原理
参考源码我们可以发现几种引用实际上大部分逻辑都在Reference基类当中。
看看Reference的源码:
public abstract class Reference<T> {
    /* A Reference instance is in one of four possible internal states:
     *
     *     Active: Subject to special treatment by the garbage collector.  Some
     *     time after the collector detects that the reachability of the
     *     referent has changed to the appropriate state, it changes the
     *     instance's state to either Pending or Inactive, depending upon
     *     whether or not the instance was registered with a queue when it was
     *     created.  In the former case it also adds the instance to the
     *     pending-Reference list.  Newly-created instances are Active.
     *
     *     Pending: An element of the pending-Reference list, waiting to be
     *     enqueued by the Reference-handler thread.  Unregistered instances
     *     are never in this state.
     *
     *     Enqueued: An element of the queue with which the instance was
     *     registered when it was created.  When an instance is removed from
     *     its ReferenceQueue, it is made Inactive.  Unregistered instances are
     *     never in this state.
     *
     *     Inactive: Nothing more to do.  Once an instance becomes Inactive its
     *     state will never change again.
     *
     * The state is encoded in the queue and next fields as follows:
     *
     *     Active: queue = ReferenceQueue with which instance is registered, or
     *     ReferenceQueue.NULL if it was not registered with a queue; next =
     *     null.
     *
     *     Pending: queue = ReferenceQueue with which instance is registered;
     *     next = this
     *
     *     Enqueued: queue = ReferenceQueue.ENQUEUED; next = Following instance
     *     in queue, or this if at end of list.
     *
     *     Inactive: queue = ReferenceQueue.NULL; next = this.
     *
     * With this scheme the collector need only examine the next field in order
     * to determine whether a Reference instance requires special treatment: If
     * the next field is null then the instance is active; if it is non-null,
     * then the collector should treat the instance normally.
     *
     * To ensure that a concurrent collector can discover active Reference
     * objects without interfering with application threads that may apply
     * the enqueue() method to those objects, collectors should link
     * discovered objects through the discovered field. The discovered
     * field is also used for linking Reference objects in the pending list.
     */
    private T referent;         /* Treated specially by GC */
    volatile ReferenceQueue<? super T> queue;
    /* When active:   NULL
     *     pending:   this
     *    Enqueued:   next reference in queue (or this if last)
     *    Inactive:   this
     */
    @SuppressWarnings("rawtypes")
    Reference next;
    /* When active:   next element in a discovered reference list maintained by GC (or this if last)
     *     pending:   next element in the pending list (or null if last)
     *   otherwise:   NULL
     */
    transient private Reference<T> discovered /* used by VM */
    /* Object used to synchronize with the garbage collector.  The collector
     * must acquire this lock at the beginning of each collection cycle.  It is
     * therefore critical that any code holding this lock complete as quickly
     * as possible, allocate no new objects, and avoid calling user code.
     */
    static private class Lock { }
    private static Lock lock = new Lock();
    /* List of References waiting to be enqueued.  The collector adds
     * References to this list, while the Reference-handler thread removes
     * them.  This list is protected by the above lock object. The
     * list uses the discovered field to link its elements.
     */
    private static Reference<Object> pending = null;
    /* High-priority thread to enqueue pending References
     */
    private static class ReferenceHandler extends Thread {
        private static void ensureClassInitialized(Class<?> clazz) {
            try {
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }
        static {
            // pre-load and initialize InterruptedException and Cleaner classes
            // so that we don't get into trouble later in the run loop if there's
            // memory shortage while loading/initializing them lazily.
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }
        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }
        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }
    }
    /**
     * Try handle pending {@link Reference} if there is one.<p>
     * Return {@code true} as a hint that there might be another
     * {@link Reference} pending or {@code false} when there are no more pending
     * {@link Reference}s at the moment and the program can do some other
     * useful work instead of looping.
     *
     * @param waitForNotify if {@code true} and there was no pending
     *                      {@link Reference}, wait until notified from VM
     *                      or interrupted; if {@code false}, return immediately
     *                      when there is no pending {@link Reference}.
     * @return {@code true} if there was a {@link Reference} pending and it
     *         was processed, or we waited for notification and either got it
     *         or thread was interrupted before being notified;
     *         {@code false} otherwise.
     */
    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    // 'instanceof' might throw OutOfMemoryError sometimes
                    // so do this before un-linking 'r' from the 'pending' chain...
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // unlink 'r' from 'pending' chain
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // The waiting on the lock may cause an OutOfMemoryError
                    // because it may try to allocate exception objects.
                    if (waitForNotify) {
                        lock.wait();
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // Give other threads CPU time so they hopefully drop some live references
            // and GC reclaims some space.
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
            // persistently throws OOME for some time...
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }
        // Fast path for cleaners
        if (c != null) {
            c.clean();
            return true;
        }
        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }
    /* -- Referent accessor and setters -- */
    /**
     * Returns this reference object's referent.  If this reference object has
     * been cleared, either by the program or by the garbage collector, then
     * this method returns <code>null</code>.
     *
     * @return   The object to which this reference refers, or
     *           <code>null</code> if this reference object has been cleared
     */
    public T get() {
        return this.referent;
    }
    /**
     * Clears this reference object.  Invoking this method will not cause this
     * object to be enqueued.
     *
     * <p> This method is invoked only by Java code; when the garbage collector
     * clears references it does so directly, without invoking this method.
     */
    public void clear() {
        this.referent = null;
    }
    /* -- Queue operations -- */
    /**
     * Tells whether or not this reference object has been enqueued, either by
     * the program or by the garbage collector.  If this reference object was
     * not registered with a queue when it was created, then this method will
     * always return <code>false</code>.
     *
     * @return   <code>true</code> if and only if this reference object has
     *           been enqueued
     */
    public boolean isEnqueued() {
        return (this.queue == ReferenceQueue.ENQUEUED);
    }
    /**
     * Adds this reference object to the queue with which it is registered,
     * if any.
     *
     * <p> This method is invoked only by Java code; when the garbage collector
     * enqueues references it does so directly, without invoking this method.
     *
     * @return   <code>true</code> if this reference object was successfully
     *           enqueued; <code>false</code> if it was already enqueued or if
     *           it was not registered with a queue when it was created
     */
    public boolean enqueue() {
        return this.queue.enqueue(this);
    }
    /* -- Constructors -- */
    Reference(T referent) {
        this(referent, null);
    }
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}
几点讲解:
1.Referent是Reference所包裹的对象引用,这儿本质上还是强引用。
2.Reference类中有一个Reference类型的静态字段pending和一个私有全局lock锁。
3Reference类中有一个静态的线程子类RefenceHandler,
 该线程子类的逻辑如下:
 (1)无限循环
 (2)每次循环的逻辑如下:
      尝试获取该类的静态全局Lock,然后进入临界区,获取pending。
      获取pending以后:
         如果是该reference对象是cleaner类型,就执行它的clean方法。
                 如果该Reference不是Cleaner类型,就将其入队,入哪个队?其自身的refencequeue。
 4.RefenceHandler是一个无限循环的线程,会伴随jvm一直运行。它所处理的pending字段从何而来?
    pending来自于垃圾收集器,垃圾收集器在进行垃圾过程当中,除了进行垃圾回收对象的GC标记以外,还会进行Reference引用对象的GC标记,当一个对象只有Reference来引用它的时候,Reference内的referent引用就会被置null,同时reference对象也会被放置在pending上,然后由ReferenceHandler线程来进行处理,处理逻辑在上面。
5.故Reference的魔力不仅仅是来源于API,而是垃圾收集器的GC标记处理和RefenceHandler线程的共同作用结果。
注意:ReferenceHandler线程与GC线程是并发的,ReferenceHandler对Reference的处理过程不会阻塞GC线程对Referent的回收(Referent的引用已经被置为null,去纠结处理时是否被回收没有意义了)
终于弄明白了Reference!!! 好累。不过还是趁热打铁来看看Cleaner

Cleaner的实现原理
PhantomReference是Reference的一个子类,Cleaner是PhantomReference的一个子类。
我们都知道虚引用不参与对象的引用分析当中,无法阻止对象的垃圾回收,即一个对象的所有强引用都已经没有了,只剩虚引用,那么这个对象还是会被垃圾回收。
Cleaner继承自PhantomReference,它本质上仍然是一个Reference。所以它的处理逻辑与WeakReference,SoftReference十分相似,仍然是由GC标记,然后由Reference Handler线程来处理。
Cleaner本身不带有清理逻辑,所有的逻辑都封装在thunk(Runnable字段对象)中,因此thunk是怎么实现的才是最关键的,Reference Handler线程会自动调用Cleaner的clean方法(看上面)。
总结来说:Cleaner是一个Reference子类,其自身可以携带一个Runnable对象来在被ReferenceHandler线程处理时进行回调,从而可以实现一些资源释放的操作。

原文
地址:https://zhuanlan.zhihu.com/p/29454205
在讲DirectBuffer的时候,圈里有同学问我,关于DirectBuffer如何回收的问题。我当时回答说别急,等我讲完GC再来讲这个问题。课程进行到现在,终于可以讲一下了。
其实,在讲DirectBuffer的时候,就已经贴过这一段代码了,但是当时没有做为重点去讲解。今天我们再返回来看一下DirectByteBuffer的构造函数:
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);

long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 这个cleaner就是DirectBuffer回收的最关键部分。
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
前边的部分大家再复习一下,今天的重点是倒数第三行的那个cleaner到底是什么鬼。我们点开Cleaner的定义看一下:
public class Cleaner
extends PhantomReference<Object>
{

// Dummy reference queue, needed because the PhantomReference constructor
// insists that we pass a queue. Nothing will ever be placed on this queue
// since the reference handler invokes cleaners explicitly.
// 就像英文注释所说的,这货没啥卵用。后面我会讲到的。
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

// Doubly-linked list of live cleaners, which prevents the cleaners
// themselves from being GC'd before their referents
// 所有的cleaner都会被加到一个双向链表中去,这样做是为了保证在referent被回收之前
// 这些Cleaner都是存活的。
static private Cleaner first = null;

private Cleaner
next = null,
prev = null;

// 构造的时候把自己加到双向链表中去
private static synchronized Cleaner add(Cleaner cl) {
if (first != null) {
cl.next = first;
first.prev = cl;
}
first = cl;
return cl;
}

// clean方法会调用remove把当前的cleaner从链表中删除。
private static synchronized boolean remove(Cleaner cl) {
// If already removed, do nothing
if (cl.next == cl)
return false;

// Update list
if (first == cl) {
if (cl.next != null)
first = cl.next;
else
first = cl.prev;
}
if (cl.next != null)
cl.next.prev = cl.prev;
if (cl.prev != null)
cl.prev.next = cl.next;

// Indicate removal by pointing the cleaner to itself
cl.next = cl;
cl.prev = cl;
return true;
}

// 用户自定义的一个Runnable对象,
private final Runnable thunk;

// 私有有构造函数,保证了用户无法单独地使用new来创建Cleaner。
private Cleaner(Object referent, Runnable thunk) {
super(referent, dummyQueue);
this.thunk = thunk;
}

/**
* 所有的Cleaner都必须通过create方法进行创建。
*/
public static Cleaner create(Object ob, Runnable thunk) {
if (thunk == null)
return null;
return add(new Cleaner(ob, thunk));
}

/**
* 这个方法会被Reference Handler线程调用,来清理资源。
*/
public void clean() {
if (!remove(this))
return;
try {
thunk.run();
} catch (final Throwable x) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null)
new Error("Cleaner terminated abnormally", x)
.printStackTrace();
System.exit(1);
return null;
}});
}
}
}
这段代码的关键注释我已经加上了。然后我再把要注意的点单独强调一下。
  1. Cleaner继承自PhantomReference,它本质上仍然是一个Reference。所以它的处理方法与WeakReference,SoftReference十分相似。仍然是由GC标记,Reference Handler线程处理的。
  2. Cleaner本身不带有清理逻辑,所有的逻辑都封装在thunk中,因此thunk是怎么实现的才是最关键的。
  3. Cleaner中的next和prev是private的,一定要记住这一点,不要和Reference的成员变量next 混淆了。它们不是一回事。这个next, prev是双向链表,而Reference的next则是由JVM维护的。
OK,关于Reference Handler的代码,请看这篇文章:WeakReference
注意看啊,Reference的定义里新启的那个线程,它的run方法会专门判断从pending链表上取出来的那个对象是不是Cleaner,如果是就会调用它的clean方法。所以我们知道了,Cleaner的clean方法是由Reference Handler线程调用的
好,到此为止,我们再回顾一下整个全景图:
首先,DirectBuffer buf 如果已经没有强引用了,那么JVM就会发现只有一个Cleaner还在引用着这个buf,那么就会把与此buf相关的那个cleaner放到一个名为pending的链表里。这个链表是通过Reference.discovered域连接在一起的。
接着,Reference Handler这个线程会不断地从这个pending链表上取出新的对象来。它可能是WeakReference,也可能是SoftReference,当然也可能是PhantomReference和Cleaner。
最后,如果是Cleaner,那就直接调用Cleaner的clean方法,然后就结束了。其他的情况下,要交给这个对象所关联的queue,以便于后续的处理。
关于Cleaner和PhantomReference就讲这么多了,下节课讲finalize,还会再补充一点cleaner和Finalizer的对比。如果有同学还想了解更多,就评论或者私信问我都行。当然,关于DirectBuffer,还有一点要补充,那就是Cleaner的第二个构造参数是一个Runnable类型的thunk:
private static class Deallocator
implements Runnable
{

private static Unsafe unsafe = Unsafe.getUnsafe();

private long address;
private long size;
private int capacity;

private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}

public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
嗯。在它的run方法里,真正地调用了freeMemory来释放内存。
DirectBuffer释放的全部差不多就都在这里了。当然,说了这么多,其实DirectBuffer的释放时机还是不确定的。首先,得发生GC,其次,Reference Handler得调度到,然后处理到你的cleaner才行,还有一条,如果你自己要实现一个cleaner,千万千万不要在run方法里写一些执行时间很长,或者会阻塞线程的逻辑的。会把Reference Handler跑死的。







原文地址;https://crowhawk.github.io/2017/08/15/jvm_3/
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。接下来讨论的收集器基于JDK1.7 Update 14 之后的HotSpot虚拟机(在此版本中正式提供了商用的G1收集器,之前G1仍处于实验状态),该虚拟机包含的所有收集器如下图所示:
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。Hotspot实现了如此多的收集器,正是因为目前并无完美的收集器出现,只是选择对具体应用最适合的收集器。

相关概念

并行和并发

吞吐量(Throughput)

吞吐量就是CPU用于运行用户代码的时间CPU总消耗时间的比值,即
吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

Minor GC 和 Full GC

新生代收集器

Serial收集器

Serial(串行)收集器是最基本、发展历史最悠久的收集器,它是采用复制算法新生代收集器,曾经(JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。它是一个单线程收集器,但“单线程”并非指该收集器只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是难以接收的。
下图展示了Serial 收集器(老年代采用Serial Old收集器)的运行过程:
为了消除或减少工作线程因内存回收而导致的停顿,HotSpot虚拟机开发团队在JDK 1.3之后的Java发展历程中研发出了各种其他的优秀收集器,这些将在稍后介绍。但是这些收集器的诞生并不意味着Serial收集器已经“老而无用”,实际上到现在为止,它依然是HotSpot虚拟机运行在Client模式下的默认的新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。
在用户的桌面应用场景中,分配给虚拟机管理的内存一般不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本不会再大了),停顿时间完全可以控制在几十毫秒最多一百毫秒以内,只要不频繁发生,这点停顿时间可以接收。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

ParNew 收集器

ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。
ParNew收集器的工作过程如下图(老年代采用Serial Old收集器):
ParNew收集器除了使用多线程收集外,其他与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作,CMS收集器是JDK 1.5推出的一个具有划时代意义的收集器,具体内容将在稍后进行介绍。
ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads参数设置。

Parallel Scavenge 收集器

Parallel Scavenge收集器也是一个并行多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
Parallel Scavenge收集器除了会显而易见地提供可以精确控制吞吐量的参数,还提供了一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
另外值得注意的一点是,Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。

老年代收集器

Serial Old收集器

Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。
此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:
它的工作流程与Serial收集器相同,这里再次给出Serial/Serial Old配合使用的工作流程图:

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程“标记-整理”算法。前面已经提到过,这个收集器是在JDK 1.6中才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择,所以在Parallel Old诞生以后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作流程与Parallel Scavenge相同,这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图:

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。
CMS收集器工作的整个流程分为以下4个步骤:
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间:
优点
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)
缺点

G1收集器

G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:
横跨整个堆内存
在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合
建立可预测的时间模型
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
避免全堆扫描——Remembered Set
G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。
为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
通过下图可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段(Safepoint处):

总结


收集器
串行、并行or并发
新生代/老年代
算法
目标
适用场景
Serial
串行
新生代
复制算法
响应速度优先
单CPU环境下的Client模式
Serial Old
串行
老年代
标记-整理
响应速度优先
单CPU环境下的Client模式、CMS的后备预案
ParNew
并行
新生代
复制算法
响应速度优先
多CPU环境时在Server模式下与CMS配合
Parallel Scavenge
并行
新生代
复制算法
吞吐量优先
在后台运算而不需要太多交互的任务
Parallel Old
并行
老年代
标记-整理
吞吐量优先
在后台运算而不需要太多交互的任务
CMS
并发
老年代
标记-清除
响应速度优先
集中在互联网站或B/S系统服务端上的Java应用
G1
并发
both
标记-整理+复制算法
响应速度优先
面向服务端应用,将来替换CMS

本文通过详细介绍HotSpot虚拟机的7种垃圾收集器回答了上一篇文章开头提出的三个问题中的第三个——“如何回收”,在下一篇文章中,我们将回答最后一个未被解答的问题——“什么时候回收”。

参考资料




原文地址:http://ifeve.com/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3g1%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/
G1 GC是Jdk7的新特性之一、Jdk7+版本都可以自主配置G1作为JVM GC选项;作为JVM GC算法的一次重大升级、DK7u后G1已相对稳定、且未来计划替代CMS、所以有必要深入了解下:
不同于其他的分代回收算法、G1将堆空间划分成了互相独立的区块。每块区域既有可能属于O区、也有可能是Y区,且每类区域空间可以是不连续的(对比CMS的O区和Y区都必须是连续的)。这种将O区划分成多块的理念源于:当并发后台线程寻找可回收的对象时、有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程、但可以用相对较少的时间优先回收包含垃圾最多区块。这也是为什么G1命名为Garbage First的原因:第一时间处理垃圾最多的区块。
平时工作中大多数系统都使用CMS、即使静默升级到JDK7默认仍然采用CMS、那么G1相对于CMS的区别在:
  1. G1在压缩空间方面有优势
  2. G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
  3. Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
  4. G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
  5. G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
  6. G1会在Young GC中使用、而CMS只能在O区使用
就目前而言、CMS还是默认首选的GC策略、可能在以下场景下G1更适合:
  1. 服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
  2. 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
  3. 想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象

一次完整G1GC的详细过程:

G1在运行过程中主要包含如下4种操作方式:
  1. YGC(不同于CMS)
  2. 并发阶段
  3. 混合模式
  4. full GC (一般是G1出现问题时发生)

YGC:

下面是一次YGC前后内存区域是示意图:
图中每个小区块都代表G1的一个区域(Region),区块里面的字母代表不同的分代内存空间类型(如[E]Eden,[O]Old,[S]Survivor)空白的区块不属于任何一个分区;G1可以在需要的时候任意指定这个区域属于Eden或是O区之类的。
G1 YoungGC在Eden充满时触发,在回收之后所有之前属于Eden的区块全变成空白。然后至少有一个区块是属于S区的(如图半满的那个区域),同时可能有一些数据移到了O区。
目前淘系的应用大都使用PrintGCDetails参数打出GC日志、这个参数对G1同样有效、但日志内容颇为不同;下面是一个Young GC的例子:
23.430: [GC pause (young), 0.23094400 secs]
...
[Eden: 1286M(1286M)->0B(1212M)
Survivors: 78M->152M Heap: 1454M(4096M)->242M(4096M)]
[Times: user=0.85 sys=0.05, real=0.23 secs]
上面日志的内容解析:Young GC实际占用230毫秒、其中GC线程占用850毫秒的CPU时间
E:内存占用从1286MB变成0、都被移出
S:从78M增长到了152M、说明从Eden移过来74M
Heap:占用从1454变成242M、说明这次Young GC一共释放了1212M内存空间
很多情况下,S区的对象会有部分晋升到Old区,另外如果S区已满、Eden存活的对象会直接晋升到Old区,这种情况下Old的空间就会涨

并发阶段:

一个并发G1回收周期前后内存占用情况如下图所示:
从上面的图表可以看出以下几点:
1、Young区发生了变化、这意味着在G1并发阶段内至少发生了一次YGC(这点和CMS就有区别),Eden在标记之前已经被完全清空,因为在并发阶段应用线程同时在工作、所以可以看到Eden又有新的占用
2、一些区域被X标记,这些区域属于O区,此时仍然有数据存放、不同之处在G1已标记出这些区域包含的垃圾最多、也就是回收收益最高的区域
3、在并发阶段完成之后实际上O区的容量变得更大了(O+X的方块)。这时因为这个过程中发生了YGC有新的对象进入所致。此外,这个阶段在O区没有回收任何对象:它的作用主要是标记出垃圾最多的区块出来。对象实际上是在后面的阶段真正开始被回收
G1并发标记周期可以分成几个阶段、其中有些需要暂停应用线程。第一个阶段是初始标记阶段。这个阶段会暂停所有应用线程-部分原因是这个过程会执行一次YGC、下面是一个日志示例:
50.541: [GC pause (young) (initial-mark), 0.27767100 secs]
[Eden: 1220M(1220M)->0B(1220M)
Survivors: 144M->144M Heap: 3242M(4096M)->2093M(4096M)]
[Times: user=1.02 sys=0.04, real=0.28 secs]
上面的日志表明发生了YGC、应用线程为此暂停了280毫秒,Eden区被清空(71MB从Young区移到了O区)。
日志里面initial-mark的字样表明后台的并发GC阶段开始了。因为初始标记阶段本身也是要暂停应用线程的,
G1正好在YGC的过程中把这个事情也一起干了。为此带来的额外开销不是很大、增加了20%的CPU,暂停时间相应的略微变长了些。
接下来,G1开始扫描根区域、日志示例:
50.819: [GC concurrent-root-region-scan-start]
51.408: [GC concurrent-root-region-scan-end, 0.5890230]
一共花了580毫秒,这个过程没有暂停应用线程;是后台线程并行处理的。这个阶段不能被YGC所打断、因此后台线程有足够的CPU时间很关键。如果Young区空间恰好在Root扫描的时候
满了、YGC必须等待root扫描之后才能进行。带来的影响是YGC暂停时间会相应的增加。这时的GC日志是这样的:
350.994: [GC pause (young)
351.093: [GC concurrent-root-region-scan-end, 0.6100090]
351.093: [GC concurrent-mark-start],0.37559600 secs]

GC暂停这里可以看出在root扫描结束之前就发生了,表明YGC发生了等待,等待时间大概是100毫秒。
在root扫描完成后,G1进入了一个并发标记阶段。这个阶段也是完全后台进行的;GC日志里面下面的信息代表这个阶段的开始和结束:
111.382: [GC concurrent-mark-start]
....
120.905: [GC concurrent-mark-end, 9.5225160 sec]
并发标记阶段是可以被打断的,比如这个过程中发生了YGC就会。这个阶段之后会有一个二次标记阶段和清理阶段:
120.910: [GC remark 120.959:
[GC ref-PRC, 0.0000890 secs], 0.0718990 secs]
[Times: user=0.23 sys=0.01, real=0.08 secs]
120.985: [GC cleanup 3510M->3434M(4096M), 0.0111040 secs]
[Times: user=0.04 sys=0.00, real=0.01 secs]
这两个阶段同样会暂停应用线程,但时间很短。接下来还有额外的一次并发清理阶段:
120.996: [GC concurrent-cleanup-start]
120.996: [GC concurrent-cleanup-end, 0.0004520]
到此为止,正常的一个G1周期已完成–这个周期主要做的是发现哪些区域包含可回收的垃圾最多(标记为X),实际空间释放较少。

混合GC:

接下来G1执行一系列的混合GC。这个时期因为会同时进行YGC和清理上面已标记为X的区域,所以称之为混合阶段,下面是一个混合GC执行的前后示意图:
像普通的YGC那样、G1完全清空掉Eden同时调整survivor区。另外,两个标记也被回收了,他们有个共同的特点是包含最多可回收的对象,因此这两个区域绝对部分空间都被释放了。这两个区域任何存活的对象都被移到了其他区域(和YGC存活对象晋升到O区类似)。这就是为什么G1的堆比CMS内存碎片要少很多的原因–移动这些对象的同时也就是在压缩对内存。下面是一个混合GC的日志:
79.826: [GC pause (mixed), 0.26161600 secs]
....
[Eden: 1222M(1222M)->0B(1220M)
Survivors: 142M->144M Heap: 3200M(4096M)->1964M(4096M)]
[Times: user=1.01 sys=0.00, real=0.26 secs]
上面的日志可以注意到Eden释放了1222MB、但整个堆的空间释放内存要大于这个数目。数量相差看起来比较少、只有16MB,但是要考虑同时有survivor区的对象晋升到O区;另外,每次混合GC只是清理一部分的O区内存,整个GC会一直持续到几乎所有的标记区域垃圾对象都被回收,这个阶段完了之后G1会重新回到正常的YGC阶段。周期性的,当O区内存占用达到一定数量之后G1又会开启一次新的并行GC阶段.

Java 性能优化备受开发人员和 IT 专家的关注。在本系列文章中,我们将帮助您了解组成 IBM SDK for Java 的许多性能关键型组件,为了帮助您了解其性能会受到哪些影响,我们还提供必要的高级背景知识。
本系列第一篇文章将介绍 IBM J9 Java 虚拟机 (JVM) 的基本架构,并重点介绍 JIT 编译器的调优。我们还会介绍共享类缓存的使用,该缓存可以提高启动速度。
本系列的其他文章:
第 2 部分:内存管理、垃圾收集 (GC) 策略和垃圾收集诊断工具
第 3 部分: Java 并发性和锁定
在本文中提供了进一步信息的链接。

J9 JVM 架构基础

J9 Java 虚拟机

IBM 开发的 J9 JVM 是自 V5 开始的所有 IBM SDK 的基础。IBM 知识中心内的J9虚拟机(JVM)主题介绍了它。J9 完全兼容 Java 虚拟机规范,并能够进行扩展,以便适用于多种设备 – 小到手机,大到 IBM z Systems 大型机。它可用在英特尔(Linux 和 Windows)、POWER(Linux 和 AIX)、ARM (Linux) 和 z Systems(Linux 和 z/OS)平台上。
J9 JVM 包含许多组件,其中包括:
类加载器
解释器
平台端口库层
垃圾收集器 (GC)
即时 (JIT) 编译器(在 J9 中代号为 Testarossa 或 TR JIT)
JVM 应用编程接口 (API)
监控和诊断组件
下图中演示了这些组件:
类加载器和解释器是 JVM 的基础组件,通常从启动应用程序时就开始使用。平台端口库层在 JVM 与底层操作系统之间提供了一个抽象层,允许在代码库中的单一位置管理大部分特定于平台的细节,比如文件 I/O 和内存分配。
可以为许多不同的 Java 类库 (JCL) 版本配置基础 J9 虚拟机,以生成适用于 Java 的不同版本的 IBM SDK 。

Testarossa JIT 编译器

J9 使用的 JIT 编译器名为 Testarossa,是一个高性能的生产编译器,其中包含全套经典优化和特定于 Java 的优化。
JIT 编译器的输出是每个方法的优化机器码,常常经过量身定制,以便利用运行该程序的特定硬件的特性。
该图大体演示了高级别的 Testarossa JIT 编译器的各种组件。IL(中间语言)生成器在设计上非常简单。它将输入的Java字节码直接转换为在编译过程的其余部分使用的Testarossa IL格式。大部分分析和优化工作都是在跨平台优化器中完成的。这个组件是 JIT 编译器最复杂的部分,大部分编译时间都用在此处。该优化器分析并转换 IL 的表达并将它传递给代码生成器,以便将其转换为适合运行该程序的平台的机器指令。
代码生成器在分配寄存器和生成二进制的同时制定特定于平台的决策,比如哪条指令最适合在特定硬件版本上采用。
在整个编译过程中,JIT 编译器通常执行与 JVM 中的其他组件和实际的 Java 命令行选项相关的各种查询,而且此代码再次抽象化为一个单独的组件。
从概念上讲,即使在没有实际执行编译时,JIT 编译器的某一部分也是存在的,这部分组件称为运行时环境。其中包括 JIT 编译器生成的已编译代码所使用的运行时帮助器例程,以及为支持各种操作(比如生成正确的异常堆栈跟踪)而生成的元数据。
Testarossa JIT 编译器一次编译一个方法。也就是说,该编译器的输入是方法的字节码和 JVM 执行状态的剩余部分(例如当前加载的类)。JIT 编译过程在很大程度上是透明的,因为 JVM 有内部“控制”启发式方法,这些方法决定了何时和如何编译哪些代码。这些启发式方法旨在平衡各种性能指标,比如应用程序吞吐量、逐步扩展/启动,以及资源占用。
不同 JVM 实现(比如 Hotspot 或 J9)中使用的 JIT 编译启发式方法之间存在一些差异,这可能给“开箱即用的”性能造成巨大影响。这些差异包括最大编译线程数量、在编译方法前调用它的次数,以及用于重新编译的启发式方法。
Testarossa JIT 编译器在大部分情况下会异步编译方法。也就是说,Java 线程不会停止执行并等待编译完成。相反,它会通过解释或使用以前编译的结果来继续执行方法。完成当前编译后,会使用生成的代码来执行该方法。
Testarossa JIT 编译器还采用了分层编译,这意味着可能会对同一个方法进行多次编译;第一次编译消耗的资源通常很少,而且仅使用部分优化来最大限度地缩短编译时间。JIT 采样线程(sampling thread)持续跟踪不同方法中所花费的时间,在执行更多优化时,会选择看起来至关重要的方法进行重新编译。Testarossa JIT 编译器使用了 70 多种不同的优化,而且拥有多种不同的优化策略。在以下文章中可以找到详细信息:
IBM 知识中心内的JIT编译器
IBM J9 JVM for Java 6 架构和特性剖析
JIT 编译并不只是动态完成传统编译。
在运行时进行编译的一个主要优势是,可以在没有任何用户干预的情况下采用推测性技术,比如基于类分层结构的优化和反馈导向的优化 (FDO)。
J9 中的分析功能(即通过仪器检测程序来跟踪运行时信息)同时在解释器和 JIT 编译器中执行,而且可能带来显著的性能提升,因为编译器甚至能了解仅在运行时才能知道的变量值(例如用户在程序运行时输入的值)。例如, 设计稳健且可扩展的解释器分析框架的经验.
在运行时进行编译的另一个优势是,JIT 编译器优化能利用它所在的架构版本的所有特性,相比而言,静态编译器必须用于所有支持的架构。

调优 Testarossa JIT 编译器

JIT 编译器的运行对最终用户而言通常是非常透明的。
这通常是可取的,因为它能基于它在执行期间收集的信息来制定决策。但是,难免做出一些权衡(比如启动和编译时间与吞吐量之间的权衡),用户可通过一些调优选项来实现这一权衡。具体来讲,有两个选项值得一提:
-Xtune:virtualized调整 JVM 的内部启发式方法,以便牺牲少量的吞吐量(通常不到 5%)来换取更短的 JIT 编译时间(通常为 25% 或更多)和更少的资源占用。这在内存或 CPU 使用过量的资源受限环境中很有用(例如虚拟化环境)。
-Xquickstart 调整 JVM 的内部启发式方法,用通常很大的性能开销(可能为 30% 或更高)换取很快的启动速度。对于快速启动和响应速度在工作流中至关重要的客户端和 GUI 应用程序中,该选项很有用。

J9 共享类技术

J9 的共享类技术对优化与类加载相关的开销(资源占用和启动时间)很有用。
除了将类存储在共享类缓存中之外,J9 还会存储提前 (AOT) 编译的代码,连接到同一个共享类缓存的不同 JVM 可以重用这些代码,而没有执行这些编译工作的开销。要使用 JVM,必须验证 AOT 编译的代码和关联的元数据可供使用,然后将代码重新放在它自己的地址空间中,以便开始使用编译的方法。
如果将共享类与上面介绍的 JIT 编译器调优选项结合使用,可进一步改善编译开销和启动时间。在以下文章中可以找到共享类技术的更多信息:
IBM 知识中心内的类数据共享
IBM developerWorks 中的Java 技术,IBM风格:类共享
IBM developerWorks 中的使用类共享提高性能
在下一篇文章中,我们将重点介绍内存管理,介绍关于可用于不同工作负载的垃圾收集策略的相关信息。我们还将分析如何使用诊断工具来监控垃圾收集性能,查明问题并推荐调优选项。
在developerWorks上的相关资源:
针对 IBM SDK for Node.js 的核心转储调试
基于 Java 的工作负载在云中的透明网络加速
本文翻译自:IBM SDK for Java performance optimizations: part one(2017-02-23)

在上一篇文章中,我们大致概述了IBM J9 JVM 架构,查看了 JIT 编译器如何优化机器代码来提高应用程序性能。本文将重点介绍内存管理,介绍关于可用于不同工作负载的垃圾收集策略的相关信息。我们还将介绍如何使用诊断工具监控性能,查明问题,并推荐与内存管理相关的调优选项。
本系列的其他文章:
第 1 部分: J9 JVM 架构基础和 JIT 编译器
第 3 部分: Java 并发性和锁定
文章中提供了更多信息的链接。

内存空间

J9 JVM 由以下内存空间组成:
内存空间
描述
Java 堆
Java 程序的类实例和数组的主要存储。
类元数据
保存类的内部表示(ROM/RAM 类)。
JVM 工作区
保存 JVM 和垃圾收集器所使用的内部数据结构。JCL 可能也会分配原生内存(例如直接字节缓冲区)。
JIT 编译的代码缓存
保存生成的原生代码。
JIT 编译的代码数据缓存
保存生成的原生代码的元数据(JIT 编译器创建的额外信息)。
JIT 编译器临时内存
JIT编译器工作区,用于在编译Java方法时内部保存数据结构。可在编译后完全释放。
JIT 编译器持久内存
在各个编译之间持久保存的 JIT 编译器内部数据结构。在编译时和运行时同时使用。示例包括类分层结构表、运行时假设表和分析数据。

J9 对象模型

用来表示 Java 对象的对象模型的JVM 实现 (JVM implementation) 会有所不同;同一个对象在两个不同的 JVM 实现中可能有不同的大小和形状。
对象模型表示 Java 对象的方式涉及 4 个方面:
字段和数组元素大小。 用于表示某个给定数据类型的字段或数组元素的字节数可能有所不同。例如,如果数据类型为 byte 可能将对象表示为一个 8 位或 32 位值。在 J9 中, Boolean, byte, short, 或char 字段按如下方式表示:
非数组对象: 32 位值
byte boolean数组元素:8 位值
char  short数组元素:16 位值
对象和字段对齐。 一般而言,会为每个 Java 对象分配一个所有 JVM 实现中的最低保证对齐边界。而且,JVM 通常会将N 字节大小的字段与肯定为 N.的倍数的地址对齐。但是,为对齐而插入的填充字节数可能不同。
但是,为对齐而插入的填充字节数可能不同。
64 位压缩引用。JVM 实现支持 64 位引用压缩(将 Java 对象引用表示为 32 位值而不是 64 位值),支持的最大大小为最大 Java 可变堆阈值。
在 J9 中,支持最高 64 GB 的最大理论堆大小的压缩引用。实际上,在大部分操作系统上,允许使用约 57 GB 的最大堆大小;在 z/OS 系统上,从 IBM JDK V8 服务更新包 2 开始,可使用 62 GB 的最大大小。
由于在用户指定了超过 4 GB 的最大堆大小时,必须执行转换来压缩和解压存取的引用字段,所以压缩引用可能产生吞吐量开销。但实际上,只要最大值超过 2.5 GB,就会导致转换和相应的开销。因此,如果想获得最高的 64 位性能,一定要知道压缩引用特性,因为从 32 位值与 64 位值之间的转换过渡到禁用压缩引用,取决于所使用的最大堆大小。
当然,使用压缩引用不但可以获得更好的对象位置,还能降低垃圾收集频率,这通常足以带来正面的优势,甚至在存在转换时也是如此。请参阅 IBM 知识中心内的使用压缩引用提高堆使用效率。
对象标头。 每个 Java 对象和数组中的标头字段数量和标头中每个字段的含义也可能有所不同。在 J9 中,非数组对象有一个标头字段,数组对象有一个额外的arraylength 字段。类指针槽的大小为:
在 32 位 JVM 或 64 位压缩引用 JVM 上,该大小为 32 位
在 64 位非压缩引用 JVM 上,该大小为 64 位
在每种情况下,底部 8 位都用于标志,比如对象存在时长和哈希值位。因此,J9 中的类与 8 字节边界对齐。
对于我们认为可能需要同步的对象类型,还有一个监控字段,在垃圾收集器删除对象和程序对其计算哈希值后,还有一个可选的哈希字段。

标头可能包含额外的槽:
数组大小(仅限数组)
监控(同步的对象)
哈希值(可以位于对象末尾
对象引用和类指针槽可以为:
32 位(带压缩引用的 32 位 JVM 或 64 位 JVM)
64 位(64 位 JVM)
示例标志:
对象存在时长
对象是否经过哈希运算

带压缩引用的 64 位 JVM 是首选,原因如下:
低资源占用(与 32 位相当)。
获得各种 64 位特性的好处(例如,x86 架构上的更多寄存器)。
备注:由于原生与压缩格式之间的转换,可能存在性能降级。
32 位 JVM 是一个可单独下载的虚拟机:
性能较差,因此已逐渐淘汰。
在传统 32 位 JNI 代码中可能需要链接到它

垃圾收集 (GC)

垃圾收集选项和默认值

通用堆大小选项:
-Xmx 最大堆大小
-Xms 初始和最小总堆大小。如果使用 Generational Concurrent 策略 (gencon),此值包含 Tenure 和 Nursery 区域。
Nursery 初始和最小大小是-Xms值的 25%;最大大小是最大-Xms值的 25%。
当前 Nursery 大小介于它的最小与最大大小之间,无论当前的-Xms值是多少。
-Xmn准确的 Nursery 大小(为平衡策略分配 (Eden) 大小)
更详细的堆大小选项:
选项
描述
-Xmca32K
RAM 类分段增量


-Xmco128K
ROM 类分段增量


-Xmcrs200M
压缩引用元数据初始大小


-Xmns1M
初始新空间大小


-Xmmnx128M
最大新空间大小


-Xms4M
初始内存大小


-Xmos3m
初始旧空间大小


-Xmox512M
最大旧空间大小


-Xmx512M
内存最大值


-Xmr16K
记住的设置大小


-Xlp: objectheap:pagesize=2M
大页面大小(支持 4K 和 2M)


-Xlp:codecache:pagesize=2M
用于 JIT 代码缓存的大页面大小(支持 4K 和 2M)


-Xmso256K
操作系统线程堆栈大小


-Xiss2K
Java 线程堆栈初始大小


-Xssi16K
Java 线程堆栈增量


-Xss1M
Java 线程堆栈最大大小


请注意,不同平台上的默认值可能不同。因此,可运行 java -verbose:sizes 来获取您的系统使用的默认值。
J9 使用的策略是,如果 Java 进程未锁定到任何特定核心或 NUMA 节点,则在系统上的所有非均匀内存存取 (NUMA) 节点上等量地插入 Java 堆内存。这可能减少性能波动(远程 NUMA 存取预计会在不同线程上大体均匀地执行,但在不同线程存取的内存是不规则分配时,可能不会均匀执行该存取)。但是,在大多数情况下,使用单一分配线程来存取内存时,也有可能降低总体性能。
从 IBM JDK V8 服务更新包 2 开始,可使用 –XX:-InterleaveMemory 禁用选项来禁用 NUMA 插入。在未来的产品版本中,可能会重新评估执行 NUMA 插入的默认策略,所以您应参阅版本文档来查找未来的更改。
使用大型页面可能也会影响性能 (例如, 请参阅 Linux 内核中的 hugetlbpage 支持).
J9 遵循的策略是,为小于 1 GB 的所有大型页面默认使用手动配置的大型页面策略,不需要指定任何选项。对于 1 GB 的大型页面,必须指定选项 –Xlp:pagesize=1G 。在具有透明大型页面支持的系统上(参阅透明大型页面支持),内核使用大型页面时完全不需要 JVM 的干预。

垃圾收集策略

IBM JVM 有许多垃圾收集策略。
默认策略是“分代并发”(gencon),这通常是首选。为了更好地控制垃圾收集暂停时间,通常会使用“基于区域”(balanced)和“软实时”(metronome)策略。
分代并发
-Xgcpolicy:gencon
采用不同方式处理短期运行的对象(在 Nursery 中)和长期运行的对象(在 Tenure 中)。采用此策略后,拥有许多短期运行的对象的应用程序通常会有更短的暂停时间,但是仍会得到不错的吞吐量。
基于区域
-Xgcpolicy:balanced  
使用与gencon类似的并发和分代技术,但基于区域来组织堆。如果全局垃圾收集(特别是由于压缩)导致应用程序暂停时间出现问题,此策略可能会牺牲一定的吞吐量来改善应用程序响应速度。在其他一些情况下,也可将它称为gencon:NUMA 利用、增量类卸载,以及在对象活跃性、堆占用和对象分配中迥然不同的工作负载的更好处理。
软实时
-Xgcpolicy:metronome
一个增量、确定性垃圾收集器,具有较短的暂停时间和有保证的最大垃圾收集吞吐量开销。依赖于精准响应时间的应用程序可以利用此技术,避免可能由于垃圾收集活动导致的长延迟。
IBM 知识中心内的-Xgcpolicy 主题中列出了所有垃圾收集策略。请在以下 IBM developerWorks 文章中进一步了解垃圾收集策略:
使用分代收集策略作为新的默认策略
新的平衡垃圾收集选项
实时垃圾收集
Java 技术,IBM 风格:垃圾收集策略,第 1 部分
Java 技术,IBM 风格:垃圾收集策略,第 2 部分

IBM 垃圾收集诊断工具

1.详细的垃圾收集日志和垃圾收集和内存可视化器 (Garbage Collection and Memory Visualizer (GCMV))。详细的垃圾收集日志提供了空闲内存、分配统计数据,以及各种关于垃圾收集活动的特定于 Java 的指标(例如终结 (finalization) 和软引用处理)和非特定指标(例如复制的对象和 remembered-set)的详细信息。
下面示例给出了一个 Scavenge(gencon 策略内的本地垃圾收集周期)的样本日志,其中显示 Nursery 中的内存/分配空间在垃圾收集之前已耗尽(0% 空闲),在垃圾收集操作后得到恢复(85% 空闲),还显示了垃圾收集操作本身的细节。

exclusive-start id="113" timestamp="2016-06-21T15:54:51.043" intervalms="2419.486">
<response-info timems="3.056" idlems="2.612" threads="37" lastid="080FA100" lastname="JIT Compilation Thread-2" />
</exclusive-start>
<af-start id="114" totalBytesRequested="152" timestamp="2016-06-21T15:54:51.043" intervalms="2419.457" />
<cycle-start id="115" type="scavenge" contextid="0" timestamp="2016-06-21T15:54:51.043" intervalms="2419.447" />
<gc-start id="116" type="scavenge" contextid="115" timestamp="2016-06-21T15:54:51.043">
<mem-info id="117" free="1563849376" total="2147418112" percent="72">
<mem type="nursery" free="0" total="536870912" percent="0">
<mem type="allocate" free="0" total="390266880" percent="0" />
<mem type="survivor" free="0" total="146604032" percent="0" />
</mem>
<mem type="tenure" free="1563849376" total="1610547200" percent="97">
<mem type="soa" free="1483322016" total="1530019840" percent="96" />
<mem type="loa" free="80527360" total="80527360" percent="100" />
</mem>
<remembered-set count="3167" />
</mem-info>
</gc-start>
<allocation-stats totalBytes="387776608" >
<allocated-bytes non-tlh="4080" tlh="387772528" />
<largest-consumer threadName="WebContainer : 29" threadId="33074B00" bytes="13873856" />
</allocation-stats>
<gc-op id="118" type="scavenge" timems="7.277" contextid="115" timestamp="2016-06-21T15:54:51.051">
<scavenger-info tenureage="5" tenuremask="ffe0" tiltratio="72" />
<memory-copied type="nursery" objects="50073" bytes="2181016" bytesdiscarded="147920" />
<memory-copied type="tenure" objects="5670" bytes="474060" bytesdiscarded="83288" />
<finalization candidates="4" enqueued="2" />
<references type="weak" candidates="12" cleared="0" enqueued="0" />
<references type="phantom" candidates="280" cleared="8" enqueued="8" />
</gc-op>
<gc-end id="119" type="scavenge" contextid="115" durationms="7.472" usertimems="12.001" systemtimems="0.000" timestamp="2016-06-21T15:54:51.051" activeThreads="2">
<mem-info id="120" free="1980031608" total="2147418112" percent="92">
<mem type="nursery" free="416743424" total="536870912" percent="77">
<mem type="allocate" free="416743424" total="419102720" percent="99" />
<mem type="survivor" free="0" total="117768192" percent="0" />
</mem>
<mem type="tenure" free="1563288184" total="1610547200" percent="97">
<mem type="soa" free="1482760824" total="1530019840" percent="96" />
<mem type="loa" free="80527360" total="80527360" percent="100" />
</mem>
<pending-finalizers system="2" default="0" reference="8" classloader="0" />
<remembered-set count="2425" />
</mem-info>
</gc-end>
<cycle-end id="121" type="scavenge" contextid="115" timestamp="2016-06-21T15:54:51.051" />
<allocation-satisfied id="122" threadId="36983A00" bytesRequested="152" />
<af-end id="123" timestamp="2016-06-21T15:54:51.051" />
<exclusive-end id="124" timestamp="2016-06-21T15:54:51.051" durationms="7.765" />
IBM Monitoring and Diagnostic Tools – Garbage Collection and Memory Visualizer (GCMV) 工具可以用图形表示大部分这类信息,将它们显示为时间的函数,以方便理解和帮助查明异常活动。该工具还会提供摘要,查明问题并推荐修复方案。
下图展示了来自 GCMV 的典型输出:
通过图形直观显示暂停时间、空闲堆内存和(Nursery 内的)翻转对象数量随时间的变化情况。可以轻松地发现一个暂停时间异常值,大约在 60 秒标记处。
要获得更多信息,请参阅以下文章:
IBM 知识中心内的 Garbage Collection and Memory Visualizer
IBM 知识中心内的详细垃圾收集日志
IBM developerWorks 中的 Garbage Collection and Memory Visualizer 2.8 版
2.Health Center。IBM Monitoring and Diagnostic Tools – Health Center 可以监控垃圾收集活动和内存使用情况。它还可以监控其他许多活动,比如锁定、方法分析和类加载。
下图展示了来自 Health Center 的典型输出:
请注意,这里显示了两个窗格:
每个类的对象实例的有序分布
关键垃圾收集指标,比如暂停时间、垃圾收集之间的间隔,以及每种类型的垃圾收集的总次数
要获得更多信息,请参阅以下文章:
IBM 知识中心内的 IBM Monitoring and Diagnostic Tools – Health Center
IBM developerWorks 中的 IBM Monitoring and Diagnostic Tools – Health Center
3.Memory Analyzer。The IBM Monitoring and Diagnostic Tools – Memory Analyzer检查内存占用、类分层结构和它们的关系。它可以诊断内存泄露并查明可能的来源。
下图展示了来自 Memory Analyzer 的典型输出:
请注意,窗格中显示了对象实例的柱状图,包含对象本身和保留的堆大小。对于选择进行进一步检查的类,会生成其他窗格,比如泄露可疑来源的报告,以及选定的对象或类的传入引用链。
要获得更多信息,请参阅以下文章:
IBM 知识中心内的 IBM Monitoring and Diagnostic Tools for Java – Memory Analyzer
IBM developerWorks 中的 IBM Monitoring and Diagnostic Tools for Java – Memory Analyzer 1.4 版
在下一篇文章中,我们将介绍 Java 并发性,重点介绍如何诊断和避免锁争用和死锁情况。我们还将介绍如何监控 CPU 利用率,以确定应用程序性能中的问题和改进目标。
在developerWorks上的相关资源:
垃圾收集简史
内存管理内幕
Unix/Linux 系统自动化管理:内存管理篇

本文翻译自:IBM SDK for Java performance optimizations: part two(2017-02-23)


在上一篇文章中,我们重点介绍了内存管理,提供了可用于不同工作负载的垃圾收集策略的相关信息。我们还介绍了如何使用诊断工具来监控性能、查明问题并推荐与内存管理相关的调优选项。在最后一篇文章中,我们将介绍 Java 并发性,重点介绍如何诊断并避免锁争用 (lock contention) 和死锁 (deadlock) 情形。我们还将介绍如何监控 CPU 利用率,以确定应用程序性能中的问题和改进目标。
本系列的其他文章:
第 1 部分:J9 JVM 架构基础和 JIT 编译器
第 2 部分:内存管理、垃圾收集 (GC) 策略和垃圾收集诊断工具
文章中提供了更多信息的链接。

Java 并发性和锁定

并发性
Java 语言和 Java 虚拟机 (JVM) 都支持并发编程。这使得 Java 应用程序能分解为可并行执行的更小任务。如果恰当使用 Java 并发性和同步特性,Java 应用程序可以获得更高的吞吐量和性能。但是,不当使用这些特性可能降低 Java 应用程序的性能和响应能力。
因此,识别和解决使用这些特性可能带来的负面问题非常重要。对于 Java 应用程序,必须研究下列因素,才能了解并发性和同步特性的影响:锁争用、死锁、CPU 利用率和 CPU 时间。
锁争用。锁通常用于阻止多个实体访问一种共享资源。Java 语言中的每个对象都有一个关联的锁,也称为监控器,线程可以使用同步方法或代码块来获取它。在 JVM 中,线程会争用 Java 对象上的各种锁。请参阅本文后面的“J9 JVM 中的锁定”。
在许多线程共享一个锁并竞争或争用这个锁时,就会发生锁争用。争用的一种典型症状是,一个应用程序消耗了大量 CPU 时间,但产生极少的输出,而许多线程却阻塞在同一个锁上。不当使用 Java 同步特性可能引起锁争用,比如长临界区段、IO 阻塞等。如果大量线程受阻塞而不是执行有用工作,Java 应用程序的响应能力和可用性就会受到负面影响。
Health Center 中的 Threads 透视图 (图 1)提供了一个用户友好的界面来实时查看线程的状态和堆栈。可以使用此信息研究锁争用。
图 1– IBM Health Center:Threads 透视图
调查争用的另一种方法是分析监控器的行为。 Health Center 中的 Locking 透视图(图 2)提供了关于 Java 应用程序中使用的所有监控器的实时统计数据。此信息可表示为条形图或表格。在条形图中,竖条的高度表示对获取监控器的非递归尝试,该尝试会导致请求线程在等待释放监控器期间受阻塞 (Slow lock count)。竖条的颜色表示请求线程在等待监控器期间锁定时,缓慢锁 (Slow lock) 占总共获取数量的百分比(“% miss”)。深色竖条表示监控器上的竞争程度比浅色竖条更激烈。图 2 中的表格包含更多信息,比如:
Average hold time:一个线程持有该监控器的平均时间。
Utilization:(“% util”)监控器被持有的时间量除以输出被使用的时间量。
总体上讲,这些统计数据显示了在 Java 应用程序中各个临界区段被访问的情况。
图 2– IBM Health Center:Locking 透视图
其他工具,例如 IBM Thread and Monitor Dump Analyzer for Java,也可用于研究锁争用。
死锁。如果多个线程在等待获取某个锁期间永远受阻塞,就会发生死锁。不同于锁争用问题,死锁对 Java 应用程序的稳定性更加有害,因为它们会阻止应用程序继续运行。
IBM JVM 可通过以下方式发现死锁:检测涉及通过同步获取的锁的周期,检测扩展 java.util.concurrent.locks.AbstractOwnableSynchronizer 类的锁,或者同时检测这二者。
图 3演示了死锁的检测,其中的两个线程“DeadLock Thread 0”和“DeadLock Thread 1”尝试在一个 java/lang/String 对象上执行同步失败,并锁定 java.util.concurrent.locks.ReentrantLock 类的一个实例。
这种典型的死锁情形是由于应用程序设计错误导致的。可以使用 Javadump 工具 帮助检测这类事件。锁再排序,避免在持有锁期间执行第三方代码,以及使用可中断的锁,诸如此类的解决方案有助于解决死锁事件。
NULL ------------------------------------------------------------------------
0SECTION LOCKS subcomponent dump routine
NULL ===============================
NULL
1LKPOOLINFO Monitor pool info:
2LKPOOLTOTAL Current total number of monitors: 2
NULL
1LKMONPOOLDUMP Monitor Pool Dump (flat & inflated object-monitors):
2LKMONINUSE sys_mon_t:0x00007F5E24013F10 infl_mon_t: 0x00007F5E24013F88:
3LKMONOBJECT java/lang/String@0x00007F5E5E18E3D8: Flat locked by "Deadlock Thread 1" (0x00007F5E84362100), entry count 1
3LKWAITERQ Waiting to enter:
3LKWAITER "Deadlock Thread 0" (0x00007F5E8435BD00)
NULL
1LKREGMONDUMP JVM System Monitor Dump (registered monitors):
2LKREGMON Thread global lock (0x00007F5E84004F58):
2LKREGMON &(PPG_mem_mem32_subAllocHeapMem32.monitor) lock (0x00007F5E84005000):
2LKREGMON NLS hash table lock (0x00007F5E840050A8):
< lines removed for brevity >

1LKDEADLOCK Deadlock detected !!!
NULL ---------------------
图 3 – 死锁检测
CPU 利用率和 CPU 时间。监控 CPU 利用率和 CPU 时间有助于识别 Java 应用程序中的退化问题或有待改进之处。
高 CPU 使用率和 CPU 时间可能是糟糕的资源利用率造成的,比如使用几倍于可用 CPU 数量的大量线程。这类症状不应忽视;它们可能表明 Java 应用程序存在真正的问题,可能阻止它扩展和处理更大的工作负载。
Health Center、Application Performance Management (APM) 解决方案和 Java Core Dump 分析(通过多个快照)等工具可用于研究 CPU 使用率和 CPU 时间。记录 CPU 时间等侵入性方法会改变应用程序的行为,所以应避免这些方法。
Health Center 中的 CPU 透视图((图 4)演示了应用程序和运行它的系统的处理器使用率。除了处理器使用率之外,该图还显示了自监控代理启动以来分析的方法数量。
图 4– IBM Health Center:Locking 透视图
Health Center 中的 Method Profiling 透视图(图 5)提供了采样方法分析器的结果。这些结果包括完整的调用堆栈信息和采样统计数据。对于大型 Java 应用程序,通过使用系统性方法并重点关注利用了最多 CPU 资源的方法,可以简化分析流程。在图 5 中,“Self (%)”是在堆栈上运行特定方法期间获取的样本百分比。这是表明一个方法使用了多少处理资源的良好指标。“Tree (%)”是在调用堆栈中的任何地方运行特定方法期间获得的采样的百分比,显示了处理此方法和它调用的方法(后代)的时间比例。这样可以很好地指导找出花费最多处理时间的应用程序区域。基于此信息,可以检查代码来识别与并发性相关的退化问题。
图 5– IBM Health Center 的 Locking 透视图
来自 IBM J9 Java Core Dump 的“Threads CPU Usage Summary”输出显示了主要 JVM 组件的线程所利用的 CPU 时间。可利用该摘要获得虚拟机 (VM) 线程、垃圾收集器 (GC) 线程和即时 (JIT) 编译器线程所花的时间。此信息很有价值,因为它有助于识别高 CPU 利用率的来源:VM、GC 或 JIT 编译器。
Threads CPU Usage Summary
=========================
All JVM attached threads: 0.083877000 secs
|
+--System-JVM: 0.083877000 secs
| |
| +--GC: 0.0 secs
| |
| +--JIT: 0.00961700 secs
|
+--Application: 0.0 secs
图 6– IBM J9 的 Java Core Dump 中的“Threads CPU Usage Summary”
在一些 Java 应用程序中,高 CPU 使用率和 CPU 时间可能是由于激烈的线程锁争用导致的。在这些情况下,-Xthr:minimizeUserCPU JVM 选项可能有助于减少 CPU 使用率和 CPU 时间。
IBM JVM 中的锁定
Java 语言提供了两种基本的同步语法:同步方法和同步语句。
如果大体了解 J9 JVM 中是如何实现锁定的,就可以最高效地使用这些语法,而且使用命令行选项来自定义 JVM 会优化应用程序的并发需求。请参阅以下 IBM developerWorks 文章,进一步了解 J9 JVM 中的锁定实现 (locking implementation):
JVM 幕后原理:IBM J9 虚拟机中的锁定 – 第 1 部分
JVM 幕后原理:IBM J9 虚拟机中的锁定 – 第 2 部分

结束语

在本系列文章中,我们探讨了 IBM SDK for Java 中一些对性能最敏感的组件,包括 JIT 编译器、垃圾收集器 (GC) 和底层 J9 虚拟机使用的锁定机制。我们还介绍了一些常见的性能问题,以及一些有关如何诊断和解决它们的指南。希望您会觉得这些文章有用,也欢迎提供反馈。

进一步阅读

DZone Refcard 文章 Java 性能优化进一步解释了可能遇到性能问题的大致区域。
这篇文章对如何解决 Java 层面的性能问题进行了技术解释,同时概述了典型 JVM 的内部工作原理并提供了调优 JVM 的选项建议。该文章重点介绍了 Hotspot JVM,但也提到了 J9 JVM。
在developerWorks上的相关资源:
使用面向 Java 的 Lock Analyzer 诊断同步和锁问题
java 程序死锁问题原理及解决方案
虚拟化环境下 cpu 利用率的监控与探究

本文翻译自:IBM SDK for Java performance optimizations: part three(2017-02-23)

个人理解
1.每个对象都关联了一个monitor record,monitor record就是传说中的重量级锁,是用C++写的,文件在hotspot\src\share\vm\runtime目录下的objectMonitor.cpp文件和objectMonitor.hpp文件
2.对象在使用重量级锁时,对象头会有指针指向monitor record
3.objectmonitor中有数据,也有行为,很多锁优化就是在里面做的。

objectMonitor.hppobjectMonitor.cpp
原文
地址:http://blog.csdn.net/javazejian/article/details/72828483

synchronized底层语义原理

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。

理解Java对象头与Monitor

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
而对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:
虚拟机位数
头对象结构
说明
32/64bit
Mark Word
存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit
Class Metadata Address
类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构
锁状态
25bit
4bit
1bit是否是偏向锁
2bit 锁标志位
无锁状态
对象HashCode
对象分代年龄
0
01
由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:
其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的,稍后我们会简要分析。这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示
由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析),ok~,有了上述知识基础后,下面我们将进一步分析synchronized在字节码层面的具体语义实现。

synchronized代码块底层原理

现在我们重新定义一个synchronized修饰的同步代码块,在代码块中操作共享变量i,如下
public class SyncCodeBlock {

public int i;

public void syncTask(){
//同步代码库
synchronized (this){
i++;
}
}
}
编译上述代码并使用javap反编译后得到字节码如下(这里我们省略一部分没有必要的信息):
Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
Last modified 2017-6-2; size 426 bytes
MD5 checksum c80bc322c87b312de760942820b4fed5
Compiled from "SyncCodeBlock.java"public class com.zejian.concurrencys.SyncCodeBlock
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
//........省略常量池中数据
//构造函数
public com.zejian.concurrencys.SyncCodeBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
//===========主要看看syncTask方法实现================
public void syncTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //注意此处,进入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //注意此处,退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //注意此处,退出同步方法
22: aload_2
23: athrow
24: return
Exception table:
//省略其他字节码.......
}
SourceFile: "SyncCodeBlock.java"
我们主要关注字节码中的如下代码
3: monitorenter //进入同步方法//..........省略其他 15: monitorexit //退出同步方法16: goto 24//省略其他.......21: monitorexit //退出同步方法
从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

synchronized方法底层原理

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现:
public class SyncMethod {

public int i;

public synchronized void syncTask(){
i++;
}
}

使用javap反编译后的字节码如下:
Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
Last modified 2017-6-2; size 308 bytes
MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
Compiled from "SyncMethod.java"public class com.zejian.concurrencys.SyncMethod
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPERConstant pool;

//省略没必要的字节码
//==================syncTask方法======================
public synchronized void syncTask();
descriptor: ()V
//方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 12: 0
line 13: 10
}
SourceFile: "SyncMethod.java"

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单了解一下Java官方在JVM层面对synchronized锁的优化

个人总结
1.ABA问题的来源不在于CAS硬件原语,CAS硬件原语是正确的,的确就具有原子性!而是来源于节点的循环利用。
2.ABA问题的解决的三种策略:
  (1)在算法中不让节点循环利用
  (2)AtomicStampedReference,带版本号的CAS
   ( 3)AtomicMarkableReference,带布尔标识的CAS

原文
地址:http://blog.csdn.net/li954644351/article/details/50511879
在《Java并发实战》一书的第15章中有一个用原子变量实现的并发栈,代码如下:
[java] view plain copy
  1. public class Node {  
  2.     public final String item;  
  3.     public Node next;  
  4.       
  5.     public Node(String item){  
  6.         this.item = item;  
  7.     }  
  8. }  

[java] view plain copy
  1. public class ConcurrentStack {  
  2.       
  3.     AtomicReference<Node> top = new AtomicReference<Node>();  
  4.       
  5.     public void push(String item){  
  6.         Node newTop = new Node(item);  
  7.         Node oldTop;  
  8.         do{  
  9.             oldTop = top.get();  
  10.             newTop.next = oldTop;  
  11.         }while(!top.compareAndSet(oldTop, newTop));  
  12.     }  
  13.       
  14.     public String pop(){  
  15.         Node newTop;  
  16.         Node oldTop;  
  17.         do{  
  18.             oldTop = top.get();  
  19.             if(oldTop == null){  
  20.                 return null;  
  21.             }  
  22.             newTop = oldTop.next;  
  23.         }while(!top.compareAndSet(oldTop, newTop));  
  24.         return oldTop.item;  
  25.     }  
  26.       
  27. }  

这个例子并不会引发ABA问题,至于为什么不会,后面再讲解,下面先讲一下ABA问题

什么是ABA?

    引用原书的话:如果在算法中的节点可以被循环使用,那么在使用“比较并交换”指令就可能出现这种问题,在CAS操作中将判断“V的值是否仍然为A?”,并且如果是的话就继续执行更新操作,在某些算法中,如果V的值首先由A变为B,再由B变为A,那么CAS将会操作成功

ABA的例子

    有时候,ABA造成的后果很严重,下面将并发栈的例子修改一下,看看ABA会造成什么问题:

[java] view plain copy
  1. public class Node {  
  2.     public final String item;  
  3.     public Node next;  
  4.       
  5.     public Node(String item){  
  6.         this.item = item;  
  7.     }  
  8. }  
[java] view plain copy
  1. public class ConcurrentStack {  
  2.       
  3.     AtomicReference<Node> top = new AtomicReference<Node>();  
  4.       
  5.     public void push(Node node){  
  6.         Node oldTop;  
  7.         do{  
  8.             oldTop = top.get();  
  9.             node.next = oldTop;  
  10.         }while(!top.compareAndSet(oldTop, node));  
  11.     }  
  12.       
  13.     public Node pop(int time){  
  14.         Node newTop;  
  15.         Node oldTop;  
  16.         do{  
  17.             oldTop = top.get();  
  18.             if(oldTop == null){  
  19.                 return null;  
  20.             }  
  21.             newTop = oldTop.next;  
  22.               
  23.             TimeUnit.SECONDS.sleep(time);  
  24.               
  25.         }while(!top.compareAndSet(oldTop, newTop));  
  26.         return oldTop;  
  27.     }  
  28. }  

注意这里的变化,Node基本没有变化

重点关注ConcurrentStack的变化
1、push方法:原来是使用内容构造Node,现在直接传入Node,这样就符合了“在算法中的节点可以被循环使用”这个要求
2、pop方法的sleep,这是模拟线程的执行情况,以便观察结果

我们先往stack中压入两个Node:
[java] view plain copy
  1. ConcurrentStack stack = new ConcurrentStack();  
  2. stack.push(new Node("A"));  
  3. stack.push(new Node("B"));  

然后创建两个线程来执行出入栈的操作

线程A先执行出栈:让NodeA出栈
[java] view plain copy
  1. stack.pop(3);  

因为某些原因,线程A执行出栈比较久,用了3s

线程B执行出栈之后再入栈:先然NodeA和NodeB出栈,然后让NodeD,NodeC,NodeA入栈(NodeA在栈顶)
[java] view plain copy
  1. Node A = stack.pop(0);  
  2. stack.pop(0);  
  3. stack.push(new Node("D"));  
  4. stack.push(new Node("C"));  
  5. stack.push(A);  

注意:线程B实现了节点的循环利用,它先将栈里面的内容全部出栈,然后入栈,最后栈顶的内容是之前出栈的Node

线程B执行完这些动作之后,线程A才执行CAS,此时CAS是可以执行成功的

按照原来的想法,线程A和B执行之后,stack的内容应该是:C和D,C在栈顶,但这里的执行结果却是Stack中什么都没有,这就是ABA问题
(实际上Stack里面还有一个B,这是作者的一个错误)
如何避免ABA问题

    Java中提供了AtomicStampedReference和AtomicMarkableReference来解决ABA问题

    AtomicStampedReference可以原子更新两个值:引用和版本号,通过版本号来区别节点的循环使用,下面看AtomicStampedReference的例子:

[java] view plain copy
  1. public class ConcurrentStack {  
  2.       
  3.     AtomicStampedReference<Node> top = new AtomicStampedReference<Node>(null,0);  
  4.       
  5.     public void push(Node node){  
  6.         Node oldTop;  
  7.         int v;  
  8.           
  9.         do{  
  10.             v=top.getStamp();  
  11.             oldTop = top.getReference();  
  12.             node.next = oldTop;  
  13.         }while(!top.compareAndSet(oldTop, node,v,v+1));  
  14. //      }while(!top.compareAndSet(oldTop, node,top.getStamp(),top.getStamp()+1));  
  15.     }  
  16.       
  17.     public Node pop(int time){  
  18.         Node newTop;  
  19.         Node oldTop;  
  20.         int v;  
  21.         do{  
  22.             v=top.getStamp();  
  23.             oldTop = top.getReference();  
  24.               
  25.               
  26.             if(oldTop == null){  
  27.                 return null;  
  28.             }  
  29.             newTop = oldTop.next;  
  30.             try {  
  31.                 TimeUnit.SECONDS.sleep(time);  
  32.             } catch (InterruptedException e) {  
  33.                 e.printStackTrace();  
  34.             }  
  35.         }while(!top.compareAndSet(oldTop, newTop,v,v+1));  
  36. //      }while(!top.compareAndSet(oldTop, newTop,top.getStamp(),top.getStamp()));  
  37.         return oldTop;  
  38.     }  
  39.       
  40.     public void get(){  
  41.         Node node = top.getReference();  
  42.         while(node!=null){  
  43.             System.out.println(node.getItem());  
  44.             node = node.getNode();  
  45.         }  
  46.     }  
  47.       
  48. }  

注意:不能使用注释中的方式,否则就和单纯使用原子变量没有区别了

AtomicMarkableReference可以原子更新一个布尔类型的标记位和引用类型,看下面的例子:

[java] view plain copy
  1. AtomicMarkableReference<Node> top = new AtomicMarkableReference<Node>(null,true);  
  2.   
  3. public void push(Node node){  
  4.     Node oldTop;  
  5.     boolean v;  
  6.       
  7.     do{  
  8.         v=top.isMarked();  
  9.         oldTop = top.getReference();  
  10.         node.next = oldTop;  
  11.     }while(!top.compareAndSet(oldTop, node,v,!v));  
  12. }  


JVM的两大无关性
语言无关性
平台无关性
无关性基石
   各种不同平台的虚拟机和所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石,字节码的载体是”Class”文件。
   Java虚拟机不和包括java语言在内的任何一门语言绑定,它只与“Class文件”这种特定的二进制格式所关联,Class文件中包含了java虚拟机指令集和符号表以及若干其它辅助信息。
任何语言只要借助相关的编译器,使其源码文件能被编译成符合虚拟机规范的字节码文件,那就能被java虚拟机执行。
Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组成的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更加强大。这为其它语言实现比java更高级的语言特性提供了基础。
Class类文件的结构
     一个Class文件对应一个类或者接口的定义信息,但类或者接口的定义信息不一定在class文件中,class文件只是一个类或接口的字节码的载体。类加载器可以直接在网络或者内存中加载字节码,并不一定要加载class文件。
     Class文件是一组以八位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中,中间没有添加任何分隔符,这使得Class文件非常紧凑,存储内容几乎是运行时所有必需数据。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割存放若干个8位字节来进行存储。(所以java的class文件的数字编码是大端,X86处理器是小端)
   Class文件中的数据结构只有两种类型:无符号数和表。
   无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串。
表是由多个无符号数或其它表作为数据项构成的复合数据类型,所有表都习惯以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。Class文件的格式如下:
   无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
1.魔数与class文件的版本
 前四个字节是魔数:0XCAFEBABE
 第五六个字节是版本号
 第七八个字节是主版本号
2.常量池
     常量池可以理解为class文件中的资源仓库,是class文件结构当中与其它项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,同时它还是在class文件中第一个出现的表类型数据项目。
     常量池中主要存放两大类常量:
字面量:文本字符串、声明为final的常量值和数字这些值等
符号引用:属于编译原理方面的概念,包括三种:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
注意:字段和方法的名称好理解,描述符就是jni当中查询字段或者方法时用到的那种描述符,如int的字段就是“I”,无参void方法就是“()V”
     常量池中的每一个常量都是一个表,目前共有14种表,每个表开始的第一位都是u1类型的标志位,代表这个常量属于什么类型,14种常量类型如下:
常量池是最繁琐的数据结构,因为14种常量类型都有自己的结构,共同点是起始的一个字节都是常量类型的标志位。
CONSTANT_Utf8_info是最常见的类型,其它的很多常量类型都有数据结构指向它,这个常量类型存放了编译出来的类名、方法名、字段名、方法描述符、字段描述符等等一系列的字符信息和字符串类型字面量等,使用UTF-8编码。
14种常量类型结构如下:
3.访问标志
   两个字节代表访问标志,该标志用于识别类或者接口层次的访问信息,包括:这个Class是类还是接口;是否是public等等,标志位可以互相自由组合,如下:
4.类索引、父类索引和接口索引集合
  类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项来确定类的继承关系。
  每个索引的u2类型数据都是指向常量池中的一个CONSTANT_Class_info的类描述符常量的索引,而通过CONSTANT_Class_info又可以知道一个Constant_Utf8_info,从而便能找到类或者接口的全限定名。如下:
只有java.lang.Object的父类索引可以是0,其它都不能为0。
5.字段表集合
  字段表用于描述接口或者类中声明的变量,包括类级变量和实例级变量,但不包括方法内部声明的局部变量。
 
Attribute_info表可能为空,这里面主要是针对常量字段而言。
6.方法表集合
方法表集合的结构与字段表集合的结构几乎一致。
方法中的代码逻辑存放在属性表集合中一个名为Code的属性当中。
7.属性表集合
在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的描述信息。
属性表的通用结构:
对于每个属性,它的名称需要从常量池中引用一个Constant_Utf8_info类型的常量来表示,而属性值的结构可以完全自定义,只需要通过一个u4的长度属性去说明属性值锁占位数。
Eg:
字节码指令简介
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。
由于java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不含操作数,只有一个操作码。
操作码的长度为一个字节,因此最多有256种操作码;又由于Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节的数据的时候,不得不在运行时重建出具体数据的结构。
这种设计会造成解释执行字节码时损失一些性能。但优势也很明显:放弃操作数对齐,意味着可以省略很多填充和间隔符号;用一个字节来代表操作码,也能够获得短小精干的编译代码。
不考虑异常处理,java虚拟机的解释器大概是下面的运行逻辑:
do{
       自动计算PC寄存器的值加1;
       根据PC寄存器的指示位置,从字节码流中取出操作码;
    If(字节码存在操作数) 从字节码流中读出操作数
    执行字节码定义的操作
} while (字节流长度 > 0)


一.概述
  虚拟机类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型叫做虚拟机的类加载。
  与那些在编译时需要进行链接工作的语言不同,在java语言里面,类型的加载、连接和初始化过程都是在运行期完成的,这虽然会让类加载增加性能开销,但是会为java应用程序提供高度的灵活性。
二.类加载的时机
  类的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段。其中验证、准备、解析三个部分统称为连接。如下:
 
加载、验证、准备、初始化和卸载这5个阶段的顺序一定,类加载过程必须按照这种顺序开始,而解析阶段不一定。注意这5个阶段可以交叉进行。
前五个阶段属于虚拟机的类加载阶段,注意加载阶段包括在类加载阶段。
类加载的第一个阶段加载什么时候进行没有强制约束,虚拟机自由把握。但是初始化阶段,虚拟机严格规定有且只有以下五种情况必须立即对类进行初始化(加载、验证、准备、解析自然要在初始化前进行,一般是某个类的加载过程当中的解析过程当中就会触发其符号引用所关联的类的加载、验证、准备、解析阶段)
  1. 使用new关键字实例化对象、读取或者设置一个类的静态字段、以及调用一个类的静态方法(被static final修饰的静态常量且已在编译期把结果放入常量池的静态字段除外)的时候。
  2. 使用反射调用类时
  3. 当初始化一个类时,若父类未加载,先触发父类的初始化
  4. 虚拟机启动时,用户指定的主类,虚拟机会先进行初始化
  5. 动态语言支持的情况
注意:
  1. 对于静态字段和方法,只有直接定义这个字段/方法的类才会被初始化,通过子类引用调用父类的静态字段/方法只会触发父类的初始化,可以通过-XX:+TraceClassLoading开开启触发子类加载。
  2. 创建引用类型数组,不会触发引用类的初始化,而是触发对应引用类型数组的初始化。
  3. 静态常量可能经过编译传播优化,可能不会引发所在类的初始化。
  4. 数组类本身并不由类加载器加载,而是虚拟机直接构成。
三.类加载的过程
1.加载
     加载是类加载过程的第一个阶段,完成以下三件事情:
  1. 通过一个类的全限定名来获取此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表此类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  注意:加载阶段和连接阶段是交叉进行的。
2.验证
  验证是连接阶段的第一步,这一阶段的目的是为了确保class文件的字节流所包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全。
  包括以下4个阶段的校验动作:
  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证:符号引用验证实际上会在解析阶段发生,通常需要校验以下内容:
  1. 符号引用通过字符串描述的全限定名是否能找到对应的类
  2. 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  3. 符号引用中的类、字段、方法和类的可访问性是否可以被当前类访问
3.准备
       准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都需要在方法区分配,初始值基本是零值。
       注意:对于static final类型,且字面量直接赋值,编译时虚拟机将会为value生成constantvalue属性,在准备阶段就会直接赋值!
4.解析
   解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程(有些是解析不出来的,对于虚方法只能运行时确定)。
   符号引用和直接引用的区别:符号引用就是一堆静态的字符串符号,直接引用是可以直接指向目标的指针、相对偏移量或是一个能够间接定位到目标的句柄。
   解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符(后三种是动态语言相关)。
  1. 类或接口的解析:可能会触发相应类或接口的加载、验证、准备和解析
  2. 字段解析:最终结果是找到该字段的直接引用或者抛出NoSuchFieldError,注意,注意:这儿的直接引用不一定是指针(实例字段要运行时才能真的拿到指针)
  3. 类方法解析:这儿所说的类方法是说属于类的方法
  4. 接口方法解析:这儿所说的接口方法时说属于接口的方法
5.初始化
 初始化时是类加载过程的最后一步,前面的过程完全由虚拟机主导控制,这个阶段可以代码自我控制。该阶段开始执行类中定义的初始化程序代码。
 初始化阶段实际上是执行类构造器“clinit()”方法的过程,clinit方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块而合并产生。编译器收集顺序是由源文件代码顺序决定,上面的只能为下面赋值,不能引用。
 实际上类中静态变量和块的顺序应该是:静态常量赋值(constant value在准备阶段)->静态变量和静态代码块的源文件顺序。
 Clinit方法的执行是会加锁的,所以线程安全,Clinit方法只执行一次。
四.类加载器
  1.类与类加载器
  类加载器虽然只用于实现类的加载,只有bootstrap和extclassloader和虚拟机绑定,其它都不和虚拟机绑定,这为java程序带来了巨大的灵活性和扩展性,一个类的唯一性由类本身和类加载器一同确定。
  2.全盘负责
  类的加载过程中遇到的类的加载都由该类加载器去加载“加载”。
  3.双亲委托
   启动类加载器(bootstrapclassloader):加载存放在<JAVA_HOME>\lib目录中的类
   扩展类加载器(ExtClassLoader): 加载存放在<JAVA_HOME>\lib\ext目录中的类
   系统类加载器(AppClassLoader): 程序默认的类加载器
   自定义类加载器
   优先父亲加载,父亲无法加载,再子类加载。
  
  4.破坏双亲委托模型
   1.ClassLoader的loadclass方法遵守双亲委托,findclass方法不遵守
   2.SPI,线程上下文类加载器
   3.OSGI,网状加载器结构
   4.tomcat重写loadclass方法

一.概述
  物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎是在物理机的执行引擎上自己实现的,因此可以自定义指令集与执行引擎的结构体系。
  Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个模型成为各种虚拟机执行引擎的外观。在外观的背后,有解释执行、编译执行、也可能两者兼备或者具备几个不同级别的编译器执行引擎。
  作用都是一致的:输入的是字节码,处理过程是字节码解析的等效过程,输出的是执行结果。
二.运行时栈帧结构
  栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈中的栈元素。栈帧存储了方法的局部变量表、操作数栈和方法返回地址等信息。每一个方法从调用开始到执行完成的过程都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。
  每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译java文件的时候,栈帧需要多大的局部变量表、多深的操作数栈都已经完全确定,并且写入到方法表的Code属性当中,因此一个栈帧需要分配多少的内存不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
  栈帧结构图:
 
1.局部变量表
  局部变量表是一组变量值存储空间,用于存储方法参数和方法内部定义局部变量。
  使用javac编译时就已经确定了该方法需要分配的局部变量表的最大容量。
  局部变量表的容量以变量槽为单位(Slot),虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是说明每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,也从来未指明每个slot占用32位空间,它准许Slot的长度可以随着处理器、操作系统和虚拟机的不同而发生变化。如果使用64位长度实现slot,那虚拟机会使用对其和补白的手段保证64位开起来和32位一样。
  经过实践和理论(一个slot必须能存放引用)证明:
  32位和64位开启指针压缩的hotspot虚拟机,一个slot占用32位。
  64位未开启指针压缩的hotspot虚拟机,一个slot占用64位。
  注意:
  1. 在32位的虚拟机上,long和double分割成两个slot存放,取值和存值必须两个一前以后操作,虚拟机会自动校验。Long和double的这种原则可能导致在32位虚拟机上有线程问题。
  2. 非静态方法的Slot0是对象的引用,this指针
  3. Slot是可重用的,只要一个局部变量已经超过了它的存活周期,那么局部变量所在的Slot就可以重用,这儿需要注意在某些情况会因为Slot上局部变量存储的对象引用还在,导致本该回收的对象没有被回收。因此局部变量手动赋null可以帮助垃圾回收,但对于JIT编译后的情况(会优化掉),该做法没有帮助。
2.操作数栈
  Java虚拟机的解释执行引擎是基于栈的执行引擎,这个栈就是操作数栈,
操作数栈的每个元素存储为32位(64位未压缩指针的虚拟机应该是64位)
操作数栈只存储基本数据类型和引用等。
Hotspot虚拟机对栈帧进行了优化,上层栈的部分局部变量表与下层栈的部分操作数栈重合。
3.动态链接
  每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法的动态调用。有些符号引用会在类加载阶段或者第一次调用是就直接转换为直接引用地址,但有些符号引用会在运行时转换为直接引用,这种就是动态链接。
4.方法返回地址
  方法返回地址的作用是为了让方法返回时正确返回到方法被调用的位置。
方法正常返回时,调用者的PC计数器的值可以作为返回地址。
方法异常退出时,返回地址通过异常处理表来确定
    方法退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者的操作数栈中,调整PC计数器到下一指令。
5.附加信息
  在实际开发中,一般把动态链接、方法返回地址与其它附加信息归为一类,称为栈帧信息。
三.方法调用
  方法调用不等同于方法执行,方法调用的唯一目标就是被确定被调用的方法的版本和内存入口。Class文件的编译过程不包括传统编译的链接步骤,存储的都是符号引用,不是方法的内存入口地址。而是在方法运行时或者类加载时来链接。Java虚拟机提供了5条方法调用指令:
1.解析
   在类加载的解析阶段会将一部分方法的符号引用转换为直接引用(方法入口地址),这些方法应该符合编译期可知,运行期不可变。这些方法包括静态方法、实例构造器方法、私有方法(本类中的私有方法)、父类方法。就是invokestatic和invokespecial指令调用的方法。
  什么是非虚方法?除开静态方法、实例构造器方法、私有方法(本类中的私有方法)、父类方法、final方法的方法都叫非虚方法。
  final方法虽然是根据其它逻辑来调用,但是final方法和上面的非虚方法一样都是在类加载的解析阶段就将符号引用替换为方法的内存入口。
  解析调用是静态的过程,在编译期间可以确定,在类加载成形。
  2.分派
  而分派调用则可能是静态的,也可能是动态的,再根据分派的宗量数可以分为单分派和多分派。两两组合就有四种情况:
  (1)静态单分派(java不支持)
  (2)静态多分派
  (3)动态单分派
  (4)动态多分派(java不支持)
  分派调用将会解释面向对象的多态特性的实现,主要是重写和重载两种。
(1)静态分派
方法的重载属于静态分派,重载依靠的是参数的静态类型
主要:静态类型是变量的声明类型,动态类型是变量的实际类型
重载往往只能得出一个最合适的版本,有合适的原则。
静态分派因为是依靠静态类型,而静态类型在编译器可知,因此叫做静态分派
(2)动态分派
动态分派和重写相关。
重写是在方法运行时,动态根据对象的实际类型来选择方法,因此是运行期才能确定,所以叫做动态分派。Invokevirtual就是动态分派,查找逻辑是:
(3)单分派和多分派
     Java是静态多分派(方法所属对象类型+参数静态类型)
           动态单分派(参数静态类型)
3.动态类型语言支持
动态类型语言的关键特征是它的类型检查的主体过程是在运行期,而不是编译期。
注意:动态类型和强弱类型不是一个概念
通俗的说动态类型只有在编译期才能确定方法接受者(调用者)的实际类型。
核心是invokedynamic指令和java.lang.invoke包。
知识点:
  1. java.lang.invoke包只是API层面的动态类型支持,不是字节码层面的直接支持,是不会编译出invokedynamic指令的,两者应该说没多大关系。
  2. MethodHandle可以类比C语言中的函数指针和dotnet中的deletegate.
  3. MethodHandle在java中的语法很像反射,但与反射有区别:反射是模拟java代码层次的方法调用,后者是模拟字节码层面的方法调用;反射是重量级的,后者是轻量级的;反射不可以做优化,后者可能会被虚拟机团队做内联优化等。
  4. Invokedynamic为虚拟机之上的动态语言服务,javac编译不出该字节指令
  5. MethodHandle的API可以完成java语言不支持的操作,比如调用祖类的方法(哪怕重写了),这个反射都是无法办到的。
四.基于栈的字节码解释执行引擎
1. 解释执行和编译执行
   语言的编译过程如下:
  
 Javac完成了程序源码到指令流的过程,然后jvm再进行解释执行,但如果含有JIT,那么也可能会反向到下面那一步。
2. 基于栈的指令架构和基于寄存器的指令架构
   略
3. 基于栈的解释器执行过程
   一般来说栈的指令架构是存在于虚拟机自定义的指令当中的,所以解释执行时基于栈的架构,但如果编译执行后,没有字节码指令了,全是本地代码,也就没有栈的指令架构一说了。
  关于解释器执行过程,本书有经典解释。

概述
在Class文件格式与执行引擎这部分当中,用户的程序能直接影响的内容不太多,大部分逻辑和过程已经被固化在虚拟机当中。
能通过程序进行操作的主要是字节码生成与类加载器这两部分的功能。
但这两部分却诞生了很多创意。
案例
Tomcat:正统的类加载器架构
Web应用服务器需要解决的问题:
  1. 同一服务器上的两个web应用程序所使用的java类库要可以相互隔离
  2. 同一服务器上的两个web应用程序锁使用的java类库可以共享
  3. 服务器需要尽可能保证自身的安全不受部署的web应用程序的影响
  4. 支持jsp应用的服务器,大多数需要使用Hotswap功能
      Tomcat的类加载架构:
     
      注意:
  1. tomcat的webappclassloader重写了load方法,逻辑已经不是双亲委托,优先加载web-inf下面的lib。
  2. tomcat6之后默认关闭CatalinaClassLoader与SharedClassLoader,上层只有CommonClassLoader,也只有一个lib文件夹
OSGI:灵活的类加载器架构
OSGI的应用案例:eclipse、IBM Jazz平台、GlassFish服务器、jboss osgi
OSGI的意义:更精确的模块划分和可见性控制、模块级的热插拔功能
OSGI的缺点:1.引入了复杂性
                    2.可能线程泄漏(多线程互相加载的情况,单线程加载可避免)
                                3.内存泄漏
       OSGI的类加载器:
              
字节码生成技术与动态代理的实现
字节码生成技术不高速,应用场景是javac编译器、CGLIB、ASM、动态代理等
核心还是拼装字节码,然后编译&加载
Retrotranslator:跨越jdk版本
Retrotranslator是“java逆向移植工具”的杰出代表,可以将java5的程序移植到java4上
Jdk版本变化内容:
  1. 语法糖,一般是javac编译时干的,可以通过工具操作字节码人工补充这些语法糖
  2. API,大部分多的API可以通过独立类库进行提供
  3. 字节码层面的修改,无法移植
  4. 虚拟机模型层面的修改,无法移植
实战
自己动手实现远程执行
知识点:
  1. 自定义类加载器需继承派生抽象类ClassLoader
  2. 如果想遵循双亲委托机制,重写loadclass方法,打破双亲委托机制,重写findclass方法
  1. 自定义类加载器加载的类需要保证不被其它类加载器重复加载,不然会出一些问题,比如classcastexception。
  2. 编译API
  3. ASM字节码操作

Java虚拟机的解释执行引擎是基于栈的执行引擎,这个栈就是操作数栈。
物理机现在基本都是基于寄存器的执行引擎,这个主要是 CPU指令集相关。
因此java虚拟机所谓的解释执行引擎是基于栈的执行引擎的说法主要是说在虚拟机层面中解释执行的字节码指令,字节码指令抽象出了一个栈。
当经过JIT编译后,已经全是本地指令,没有字节码的存在了。所以JIT编译后的执行引擎已经是物理机执行引擎,实际上不算栈的执行引擎了,但可能用到的寄存器存储指令还是相对较少,因此效率可能还是比C略差。

一.概述
  Java的编译器种类繁多,前面几章讲解的大部分和解释执行相关,现在讲编译。
  Java的编译器有三种:
  1. 前端编译器:sun的javac、eclipse JDT中的增量式编译器(ECJ)
  2. JIT编译器:Hotspot VM的C1、C2编译器
  3. AOT编译器:GNU Compier for java(GCJ)、Excelsior JET
前端编译器:输入是*.java文件,输出是*.class文件
   JIT编译器:输入是*.class文件,输出是本地代码
   AOT编译器:输入时*.java文件,输出是本地代码
  在java当中,即时编译器在运行期的优化过程对于程序运行来说很重要(运行优化),而前端编译器在编译期的优化过程对于程序编码来说关系更加密切(语法糖和新的语言特性)。
Java开发团队主要把目光放在虚拟机运行时优化,也就是JIT编译器的优化,堆前端编译器的优化很少,基本只有解语法糖。
二.javac编译器
  Javac编译器本身就是由java写的(那javac又是怎么编译出来的呢,C/C++语言干的?)
  Javac编译器的源码在openjdk目录/langtools/src/classes/com/sun/tools/javac当中,javac只依赖sun目录下面的java文件,所以可以把com目录复制到自己的项目当中来进行调试。
  Javac的核心类是/langtools/src/com/sun/tools/javac/main/JavaCompiler.java
  核心方法:
  public void compile(List<JavaFileObject> sourceFileObjects,
                        List<String> classnames,
                        Iterable<? extends Processor> processors)
    {
        if (processors != null && processors.iterator().hasNext())
            explicitAnnotationProcessingRequested = true;
        // as a JavaCompiler can only be used once, throw an exception if
        // it has been used before.
        if (hasBeenUsed)
            throw new AssertionError("attempt to reuse JavaCompiler");
        hasBeenUsed = true;
        // forcibly set the equivalent of -Xlint:-options, so that no further
        // warnings about command line options are generated from this point on
        options.put(XLINT_CUSTOM + "-" + LintCategory.OPTIONS.option, "true");
        options.remove(XLINT_CUSTOM + LintCategory.OPTIONS.option);
        start_msec = now();
        try {
            initProcessAnnotations(processors);
            // These method calls must be chained to avoid memory leaks
            delegateCompiler =
                processAnnotations(
                    enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
                    classnames);
delegateCompiler.compile2();
            delegateCompiler.close();
            elapsed_msec = delegateCompiler.elapsed_msec;
        } catch (Abort ex) {
            if (devVerbose)
                ex.printStackTrace(System.err);
        } finally {
            if (procEnvImpl != null)
                procEnvImpl.close();
        }
    }
Javac基本存在三个编译过程:
  1. 解析与填充符号表过程
  2. 插入式注解的注解处理过程
  3. 分析与字节码生成
解析与填充符号表包括:词法、语法分析;填充符号表
注解处理器:注解处理,这部分是程序员可以操控编译的唯一方法
语义分析及字节码生成:标注检查、数据及控制流分析、解语法糖、字节码生成(由/langtools/src/com/sun/tools/javac/jvm/Gen.java完成)
三.java语法糖的味道
  1.泛型与类型擦除
  2.自动装箱、拆箱
  3.遍历循环
  4.lambda表达式算不算语法糖?
四.实战:插入式注解处理
知识点:
  1. 这儿是参与javac编译的第二个阶段:插入式注解的注解处理过程
  2. 方法是写一个Processor类继承自javax.annotation.processing.AbstractProcessor,派生process方法,javac编译时指定参数-processor
  1. 使用这组API的项目包括:Hiberante Validator Annotation Processor和Project Lombok

一.概述
  热点代码
  即时编译器不是虚拟机必需的部分,java虚拟机规范并没有规定java虚拟机内必须要有即时编译器的存在,但即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键指标之一。
二.hotspot虚拟机内的即时编译
  这儿主要回答5个问题:
 
  1.解释器与编译器
  主流商用虚拟机都同时包括解释器与编译器。
  Hotspot虚拟机内置一个解释器与两级编译器(C1编译器或者说Client编译器、C2编译器或者说Server编译器)
  解释器可以作为编译器的逃生门
  C1编译器也可以作为C2编译器的逃生门
  有三种模式:
  1. 纯解释模式 -Xint参数
  2. 纯编译模式 -Xcomp参数
  3. 缺省混合模式(mix): Hotspot虚拟机会自动根据自身版本和宿主机器的硬件性能选择合适的运行模式,也可以使用-Client或者-Server模式。
Java7之后的Server模式会默认开启分层编译:
使用编译器和解释器混合模式的意义在于可以保证快速的启动速度和高速的运行性能的平衡,解释器可以作为编译器的逃生门,解释器可以为编译器收集性能信息来为编译器优化提高性能依据。
  2.编译对象与编译触发条件
   编译对象都是都是方法!!!
   热点代码有两类:
  1. 被多次调用的方法(标准JIT编译方式)
  2. 被多次执行的循环体(OSR,栈上替换,方法栈还在栈上,方法就被替换了)
热点探测的方式有三种:
  1. 基于采样的热点探测
  2. 基于计数器的热点探测(hotspot)
  3. 基于踪迹的热点探测(dalvik)
方法调用计数器触发即时编译:
方法调用计数器触发即时编译的阈值在client下默认是1500次、server下默认是10000次,这个阈值可以通过虚拟机参数-XX:CompileThrehold来设定,方法调用计数器有半衰期。
 回边计数器触发即时编译:

 
 回边计数器无半衰期,空循环不算回边
  3.编译过程
   略
  4.查看及分析即时编译结果
   略
三.编译优化技术
  1.优化技术概览
 
  2.公共子表达式消除
  略
  3.数组边界检查消除法
  Java访问数组和访问对象都会检查边界和内存值,从而会抛出数组越界和空指针异常,从而才能保证安全性,当上下文确定不会抛出异常,可以取消这些检查
  4.方法内联
  非虚方法可以内联,虚方法当只有一个待选版本可以激进优化,基于继承链分析
  5.逃逸分析
  逃逸分析与继承链分析都不是优化技术,但可以为其它优化手段提供参考依据。
  逃逸分析中包括方法逃逸和线程逃逸,只要没逃逸,可能进行一些优化:
  1. 栈上分配
  2. 同步消除
  3. 标量替换
  4. 锁消除等等
四.java与C/C++编译器对比
  Java编译器的弱势
  1. 即时编译占用了用户程序的运行时间
  2. 动态的类型安全语言,有虚拟机托管,所以有很多安全检查,要消耗一点时间
  3. Java的虚方法比C++多得多,内联效果没有那么理想
  4. 全局优化难以支持
  5. 垃圾回收机制与程序直接控制内存相比,速度更慢
Java编译器的优势
  1. 更容易的进行别名分析
  2. 运行期性能监控可以获得宝贵的运行信息,如调用频率预测,分支频率预测、裁剪未被选择的分支、从而可以进行锁优化等手段。

高效并发是《深入理解Java虚拟机》的最后一部分,将介绍虚拟机如何实现多线程、多线程之间由于共享和竞争数据而导致的一系列问题及解决方案。
一、硬件的效率与一致性
如果学过操作系统,这一块就很容易理解了。我们知道,计算机的执行速度是一个正三角模型,依次为:
CPU - 高速缓存 - 内存 - 外存
所以,要实现计算机并发执行多个任务和充分利用计算机CPU的性能就不是那么简单了。因为CPU和内存、外存的速度差别太大(跨越N个数量级),所以提出了高速缓存的概念。高速缓存是读写速度尽可能接近CPU运算速度的存储区域,它作为内存与CPU之间的缓冲:将运算需要使用到的数据复制到缓存中,让CPU进行运算,当运算计算后再从缓存同步到内存中,这样就无须等待缓慢的内存读写了。
引入高速缓存很好的解决了CPU与其它存储单元速度差异太大的问题,但同时也引入了新的问题——缓存一致性。在多CPU机器上,每个CPU都有自己的高速缓存,而它们又共享一个主内存,当多个CPU的运算任务都涉及内存的同一块区域时,就可能导致缓存不一致的情况,如果真是这样,那同步回内存的缓存以谁的数据为主呢?为了解决这个问题,需要各个CPU访问缓存时遵守一定的协议,比如MSI、MESI、MOSI等等。
整个过程可以用下图说明:
二、Java内存模型
首先最重要的一点是要知道为什么要有Java内存模型。
Java虚拟机规范定义了Java内存模型(Java Memory Model,JMM)来实现屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。要抓住重点:屏蔽硬件差异,保证并发。而程序的功能就是数据流的交互,所以保证数据的快速、正确访问就是Java内存模型的核心。
在此之前,C/C++直接使用物理硬件(或者说是操作系统的内存模型),因此会导致不同平台、不同操作系统的差异:在一个平台上并发完全正常,到了另一个平台可能程序就会经常出错。因此还得针对不同的平台开发不同的C/C++版本。而Java为了实现平台无关性(Write Once,Run Anywhere),就定义了JMM。但是定义一个MM绝非易事:
(1) 必须足够严谨:这样才能保证Java的兵法操作不会产生歧义
(2) 必须足够宽松:使JVM的实现可以有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存等)来获取更好的执行速度
1. 主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量和Java程序中的变量略有区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为它们是线程私有的,不会被共享,自然不存在竞争问题。(JVM堆中的数据,是多线程共享的)
Java内存模型规定了所有的变量存储在JVM的主内存中。每条线程还有自己的工作内存(类比高速缓存)。线程工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间的工作内存也是相互独立的,线程间变量值传递均需要主内存完成。线程、主内存、工作内存之间的关系如下图所示:
2. 内存间交互操作
关于主内存和工作内存之间的消息,主要是“主 - 工作”和“工作 - 主”,JMM定义了8种操作:
(1) lock:作用于主内存的变量,把一个变量标识为一条线程独占的状态
(2) unlock:作用于主内存的变量,把一个lock的变量解锁,可供其他线程使用
(3) read:作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存,供load动作使用
(4) load:作用于工作内存的变量,把read操作从主内存得到的变量值放入工作内存的变量副本中
(5) use:作用于工作内存的变量,把工作内存一个变量的值传递给执行引擎,当JVM遇到使用该变量的字节码指令会执行use操作
(6) assign:作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行assign操作
(7) store:作用于工作内存的变量,把工作内存中一个变量的值传递给主内存中,供write动作使用
(8) write:作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中
从上面的规则,我们可以简单说几个例子说明一下。比如要把一个变量从主内存复制到工作内存,就要按顺序执行read和load操作;如果把一个变量从工作内存同步到主内存,就要按顺序执行store和write操作。
与此同时,JMM还规定了在执行上述8种基本操作时必须满足如下规则:
(1) 不允许read和load、store和write操作之一单独出现,必须成对出现。即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起同步,主内存不接受
(2) 不允许一个线程丢弃assign操作,即变量在工作内存中改变了以后必须同步回主内存
(3) 不允许一个线程无原因(没有任何assign操作)就把数据从工作内存同步回主内存(因为是无用功嘛)
(4) 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,即对一个变量实现use和store操作之前,必须执行过assign或者load操作
(5) 一个变量在同一个时刻只允许一条线程对其lock,但lock可以被同一条线程重复执行多次,多次lock后也得有相同次数的unlock才能解锁
(6) 对一个变量执行lock操作,将会清空工作内存中这个变量的值,在执行引擎使用这个变量前,需要重新执行load或assign初始化变量的值
(7) unlock时必须保证是同一个线程在先前对这个变量执行过lock
(8) 对一个变量执行unlock之前,必须把此变量同步回主内存
3. volatile型变量
关键字volatile是Java虚拟机提供的最轻量级的同步机制,但是它并不容易被正确地、完整地理解,所以在遇到多线程数据竞争的问题时一律使用synchronized来进行同步。而了解volatile变量的语义对后面了解多线程操作的其他特性有很重要的意义,所以我们先通俗的说一下volatile的语义。
volatile是轻量级同步机制,它保证被修饰的变量在修改后立即列入主内存,使用变量前必须从主内存刷新到工作内存,这样就保证了所有线程的可见性。不存在隔离性
对一个变量被定义为volatile之后,将具备两种特性
(1) 保证此变量对所有线程的可见性(重要)
这里的可见性指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量则需要通过主内存作为桥梁:线程A修改了一个普通变量的值,需要先向主内存进行回写,线程B在线程A完成回写后才能从主内存读取到新值,而之前还是线程A修改前的值。(volatile也要求变量值刷新回主内存,只不过,新值是立即刷新的,而且要求使用之前,从主内存重新获取)
但是关于volatile变量的可见性,如果没有深入了解,是会被误解的,常见的一种错误描述是“volatile变量对所有线程是立即可见的,对volatile变量的所有写操作都能立刻反应到其它线程中。换句话说,volatile变量在各个线程中是一致的,所有基于volatile变量的运算在并发下是安全的”。这句话其实是错误的,不能得出“基于volatile变量的运算在并发下是安全的”这个结论。
volatile变量在各个线程的工作内存中是不存在一致性问题(是因为执行引擎在使用这个变量时,都会显式刷新后使用),但是Java运算并非原子操作
结论:由于volatile变量只保证可见性,在不符合以下两条规则的运算场景中,我们仍然需要通过加锁(使用synchronized或者java.util.concurrent中的原子类)来保证原子性
* 运算结果并不依赖变量的当前值,或者能够确保**只有单一的线程修改变量的值**
* 变量不需要与其他的状态变量共同参与不变约束
(2) 禁止指令重排序优化
普通的变量仅仅保证在该方法执行过程中所有依赖赋值结果的地方都能获取正确的值,而不能保证变量赋值操作的顺序与程序代码的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是JMM描述的所谓“线程内表现为串行的语义”。
结论:确实在某些情况下,volatile同步机制的性能要优于锁(使用synchronized或者java.util.concurrent包的锁),但因为JVM的各种优化,这方面是无法一刀切的。所以,使用volatile的原则是满足使用场景的需求即可
4. 对于long和double型变量的特殊规则
long和double都占据了2个连续的slot,但是因为局部变量表是线程私有,所以不会出现数据安全的问题。而且JMM虽然允许JVM把long和double变量的读写实现为非原子操作,但是还是“强烈建议”JVM实现为原子操作。所以在实际应用中,各平台的商用JVM几乎都选择把64位数据的读写操作作为原子操作。所以在编写代码时就不用担心读取long或者double类型半个slot的问题了。
注:上述JMM允许占用64位的long、double数据的load、store、read、write实现为非原子的,此为,long和double的非原子性协定。
5. JMM模型的三大特性
前面介绍JMM的相关操作和规则,现在总结一下JMM模型的三大特性(要和事务的ACID对比):
(1) 原子性:JMM会保证read/load/assign/use/store/write的原子性,如果需要更大范围的原子性,可以使用lock和unlock,这个从代码层面来看就是synchronized
(2) 可见性:可见性就是当一个线程修改了共享变量的值,其他线程能立即得知这个修改。而volatile就是搞这个的。JMM通过变量修改后回写主内存,读取前从主内存刷新变量值这种依赖主内存作为中介的方法实现可见性,只不过volatile的特殊规则能使新值立即同步主内存,使用前立即从主内存刷新。所以可以说volatile保证了多线程操作时变量的可见性,而普通变量不能保证这一点。除了volatile之外,Java还有两个关键字能实现可见性:synchronized和final,synchronized是由“对一个变量执行unlock操作之前,必须把此变量的值回写到主内存”这条规则实现的,细想一下,就明白了。而final是由“被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this引用传递出去,那么在其他线程中就能看见final字段的值”
(3) 有序性:JMM的有序性可以总结为一句话:本线程内观察,所有的操作都是有序的;如果是旁观者线程,被观察线程的操作都是无序的。前半句是指”线程内表现为串行的语义“,后半句是指”指令重排序“和”工作内存和主内存存在同步延迟“的现象。Java语言提供了volatile和synchronized保证线程之间操作的有序性。 介绍这三种特性后可能会发现,synchronized在这三种特性中都能出色的完成任务。的确,大部分的并发控制操作都能使用synchronized来完成。但是也因为它的万能,使得它的使用被程序员滥用的局面逐渐增多,越通用的东西,需要考虑的东西太多,性能肯定会受到影响。
Synchronized能够实现原子性、可见性、有序性;原子性是因为独占运行而获得,有序性也是因为这点,因为独占保证了线程之间操作的有序性,而具有可见性是因为在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
三、Java与线程
其实并发不一定必须依靠多线程(PHP还依靠多进程并发呢),但是在Java中,并发和线程脱不开关系。所以,我们先来八一八线程的实现。注意,不是Java线程的实现,而是线程的实现哦。
我们知道,线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的最基本单位)。
操作系统都提供了线程实现,Java语言实现的java.lang.Thread类的实例就代表一个线程。不过Thread类和大部分Java API不同的是,它的关键方法都被声明为Native:意味着这个方法没有使用或无法使用平台无关的手段来实现(当然也可能是为了执行效率,不过通常最高效率的手段肯定是平台相关的手段啦)。
实现线程主要有三种方式:
(1) 使用内核线程实现
(2) 使用用户线程实现
(3) 使用用户线程+轻量级进程混合实现
1. 使用内核线程实现
内核线程(Kernel Thread,KLT)是操作系统内核(Kernel)支持的线程,这种线程是由内核完成线程切换的:内核通过调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程都可以看做内核的一个分身。
程序一般不适用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process, LWP),轻量级进程就是我们通常意义所说的线程。每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程和内核线程1:1的关系称为一对一的线程模型
下面说一下它的特点:
(1) 由于轻量级进程和内核线程是一对一的,所以每个轻量级进程都是一个独立的调度单元,即使阻塞了,也不会影响整个进程的工作
(2) 因为它基于内核线程实现,所以各种进程操作如创建、析构、同步,都需要系统调用。而系统调用代价相对较高,需要频繁在用户态和内核态切换。而且因为每个轻量级进程都需要一个内核线程的支持,所以会消耗一定的内核资源(如内核线程的栈空间)。因此一个系统支持轻量级进程的数量是有限的
2. 使用用户线程使用
广义上说,不是内核线程,就可以被认为是用户线程。从这个定义看,轻量级进程(LWP)也属于用户进程。而狭义的用户线程是指完全建立在用户空间的线程库上,系统内核是无法感知到线程的存在的。用户线程的创立、同步、销毁、调度完全在用户态完成,不需要内核的帮助。如果实现的牛逼,这种线程不需要切换到内核态,操作效率非常高,消耗也很低,而且支持大规模的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系被称为一对多的线程模型
优缺点:
(1) 实现好的话,效率奇高
(3) 但是也正因为没有内核的帮助,所以操作都需要用户程序自己处理。实现起来非常复杂,Java原来使用的也是用户线程,后来因为实现起来太复杂就放弃了
3. 使用用户线程+轻量级进程混合实现
用户线程完全建立在用户空间中,因此用户线程的创建、切换、析构依然廉价,支持大规模的用户线程并发。而轻量级进程则作为用户线程和内核线程的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用通过轻量级进程完全,降低了进程被阻塞的风险。这种混合模式下,用户线程和轻量级进程的数量比不确定,一般称为多对多线程模型
4. Java线程的实现
这里说一下Java线程是如何实现的。
(1) JDK 1.2之前,用户线程实现
(2) JDK 1.2,由于用户线程模型太过复杂,替换为操作系统原生线程模型实现。对Sun JDK而言,它的Win和Linux都是使用一对一的线程模型实现的;而在Solaris平台,可以选择使用一对一线程模型或者多对多线程模型
5. Java线程调度
分为两种:
(1) 协同式线程调度:一个线程把自己活全部搞定的情况下再通知下一个线程干活。好处是简单,坏处是一个线程有问题,整个程序就会阻塞,甚至崩溃
(2) 抢占式线程调度(Java线程调度方式):根据优先级抢占,不过不同平台优先级定义不同
6. 状态切换
Java语言定义了5种进程状态,在任意一个时间点,一个进程只可能处于一种状态。如果操作系统没忘,这块应该很清楚的。
(1) 新建:创建后尚未启动的进程
(2) 运行:处于运行状态的进程可能正在执行,也可能在等待CPU分配执行时间
(3) 无限期等待:处于这种状态的进程不会被分配CPU执行时间,需要被显式唤醒
(4) 限期等待:处于这种状态的进程不会被分配CPU执行时间,但是他们不需要被显式唤醒,自己会唤醒自己
(5) 阻塞:一般是临界区
(6) 结束:正常执行完毕

java语言中的线程安全
  1.不可变
  2.绝对线程安全
  3.相对线程安全
  4.线程兼容
  5.线程对立

线程安全的实现方法
  1.互斥同步
    synchronized & j.u.c
    悲观策略

  2.非阻塞同步
    CAS等硬件原语 & 自旋 & volatile (注意ABA问题)
    乐观策略

  3.无同步方案
    不可变对象 & ThreadLocal & volatile

锁优化
高效并发是从 JDK 1.5 到 JDK 1.6 的一个重要改进,HotSpot 虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

自旋锁与自适应自旋

        前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的营销阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁
        自旋锁在 JDK 1.4.2 中就已经引入,只不过默认是关闭的,可以使用 -XX:+UseSpinning 参数来开启,在 JDK 1.6 就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时候很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是 10 次,用户可以使用参数 -XX:PreBlockSpin 来更改
        在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越 “聪明” 了。

锁消除

        锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判定在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
        也许读者会有疑问,变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的。同步的代码在 Java 程序中的普遍程度也许超过了大部分读者的想象。我们来看看代码清单 13-6 中的例子,这段非常简单的代码仅仅是输出 3 个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。
代码清单 13-6  一段看起来没有同步的代码
  1. public static String concatString(String s1, String s2, String s3) {  
  2.     return s1 + s2 + s3;  
  3. }  

        我们也知道,由于 String 是一个不可变的类,对字符串的连接操作总是通过生成新的 String 对象来进行的,因此 Javac 编译器会对 String 连接做自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即代码清单 13-6 中的代码可能会编程代码清单 13-7 的样子(注:客观地说,既然谈到锁消除与逃逸分析,那虚拟机就不可能是 JDK 1.5 之前的版本,实际上会转化为非线程安全的 StringBuilder 来完成字符串拼接,并不会加锁,但这也不影响笔者用这个例子证明 Java 对象中同步的普遍性)。
代码清单 13-7  Javac 转化后的字符串连接操作
[java] view plain copy
  1. public static String concatString(String s1, String s2, String s3) {  
  2.     StringBuffer sb = new StringBuffer();  
  3.     sb.append(s1);  
  4.     sb.append(s2);  
  5.     sb.append(s3);  
  6.     return sb.toString();  
  7. }  

        现在大家还认为这段代码没有涉及同步吗?每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会 “逃逸” 道 concatString() 方法之外,其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。

锁粗化

        原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。
        大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
        代码清单 13-7 中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,以代码清单 13-7 为例,就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。

轻量级锁

        轻量级锁是 JDK 1.6 之中加入的新型锁机制,它名字中的 “轻量级” 是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为 “重量级” 锁。首先需要强调一点的是,轻量级锁并不是用来代替重要级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
        要理解轻量级锁,以及后面会讲到的偏向锁的原理和运作过程,必须从 HotSpot 虚拟机的对象(对象头部分)的内存布局开始介绍。HotSpot 虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄(Generational GC Age)等,这部分数据是长度在 32 位和 64 位的虚拟机中分别为 32 bit 和 64 bit,官方称它为 “Mark Word”,它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
        对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Work 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如,在 32 位的 HotSpot 虚拟机中对象未被锁定的状态下,Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码(HashCode),4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0,在其他状态(轻量级锁定、重量级锁定、GC 标记、可偏向)下对象的存储内容见表 13-1。
        简单地介绍了对象的内存布局后,我们把话题返回到轻量级锁的执行过程上。在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态)虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方把这份拷贝加上了一个 Displaced 前缀,即 Displaced Mark Word),这时候线程堆栈与对象头的状态如图 13-3 所示。
        然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位 (Mark Word 的最后 2bit)将转变为 “00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图 12-4 所示。
        如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象以及被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,所标志的状态变为 “10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
        上面描述的是轻量级锁的加锁过程,它的解锁过程也是通过 CAS 操作来进行的,如果对象的 Mark Word 仍然指向着线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和线程中复制的 Displaced Mark Word 替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要释放锁的同时,唤醒被挂起的线程。
        轻量级锁能提升程序同步性能的依据是 “对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

偏向锁

        偏向锁也是 JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了
        偏向锁的 “偏”,就是偏心的 “偏”、偏袒的 “偏”,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
        如果读者读懂了前面轻量级锁中关于对象头 Mark Word 与线程之间的操作过程,那偏向锁的原理理解起来就会很简单。假设当前虚拟机启用了偏向锁(启用参数 -XX:+UseBiasedLocking,这是 JDK 1.6 的默认值),那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为 “01”,即偏向模式。同时使用 CAS 操作把获取到这个锁的线程 ID 记录在对象的 Mark Word 之中,如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行如何同步操作(例如 Locking、Unlocking 及对 Mark Word 的 Update 等)。
        当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为 “01”)或轻量级锁定(标志位为 “00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级锁的状态转换及对象 Mark Word 的关系如图 13-5 所示。
        偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。 


线程安全的一些思考
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:
1.不在线程之间共享该状态变量
2.将状态变量修改为不可变的变量
3.在访问变量时使用同步
同步包括:synchronized、volatile、juc包、硬件原语等
当设计线程安全的类时,良好的面向对象技术,不可修改性,以及明晰的不变性规范都能起到一定的帮助作用。
什么是线程安全性
      标准含义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
     上面这个含义实际上是绝对线程安全性的定义,java api当中大部分线程安全类都不满足上面的定义(连续操作不满足),它们称为相对线程安全。
     深入理解jvm中定义了五种线程安全等级:
     1.不可变
     2.绝对线程安全
     3.相对线程安全
     4.线程兼容
     5.线程对立
     无状态对象一定线程安全(java ee)!!!
原子性
     1. 竞态条件
         常见的两类竞态条件: 
          读取-修改-写入
          先检查后执行
    2.原子性和复合操作
        假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么B全部执行完,要么完全不执行B,那么A和B彼此来说就是原子的。原子操作是指对于访问同一个状态的所有操作来说,这个操作是一个以原子方式执行的操作
加锁机制
      要保证状态的一致性,就需要在单个原子操作中更新所有的相关状态变量
      内置锁:独占、可重入、不可中断
用锁来保护状态
      对于可能被多个线程同时访问的这些可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁来保护的
      每个共享的和可变的变量都应该只由一个锁来保护,从而让维护人员知道是哪一个锁
      常见加锁约定:将所有的可变状态都封装在对象内部,并通过对象的内置锁(最好使用内部锁)对所有访问可变状态的代码路径进行同步
      对于每个包含多个变量的不变性条件中,其中涉及的所有变量都需要由同一个锁来保护
活跃性和性能问题
     简单性和性能之间存在相互制约,一定不要盲目为了性能而牺牲简单性
     当执行长时间的计算或者无法快速完成的操作(如网络I/0或者控制台I/0),一定不要持有锁         

可见性和重排序
什么是可见性?
深入理解jvm中有详细讲解,主要就是一个工作内存和主内存,变量必须通过主内存来向其它线程更新。
volatile变量,synchronized内的变量,显式锁内的变量可以保证可见性

什么是重排序?
在没有同步的情况下,编译器、处理器、以及运行时都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序当中,要想对内存操作的执行顺序进行判断,几乎无法得出正确结论。
这看上去是一种失败的设计,不过却能使得jvm充分地利用现代多核处理器的强大性能。例如:在缺少同步的情况下,java内存模型准许编译器对操作顺序进行重排序,并将数值存在寄存器当中,它还准许CPU对操作顺序进行重排序,并将数值缓存在CPU高速缓存当中。
可以简单认为存在两种重排序
1.编译器级别的(解释执行不会)
2.CPU级别的
volatile变量,synchronized&显式锁,final变量的特殊规则都可以保证不被重排序

失效数据
过期数据,指已经在某个线程被更新,但其他线程却还是读到的是旧值

非原子的64位操作
long和double类型在32位虚拟机上准许读和写都可以分两次完成,所以读写不是原子操作,大部分商用虚拟机都保证了它是原子操作

加锁与可见性
synchronized内的变量,显式锁内的变量可以保证可见性
加锁的含义不仅仅在于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作和写操作的线程都必须在同一个锁上同步。
volatile变量的可见性
volatile变量在内存可见性上的作用比锁略低(因为只有volatile变量才有可见性),大多数处理器上,读取volatile变量只比读取普通变量的开销略高一点。
加锁机制既可以确保可见性,又可以确保原子性,而volatile变量只能确保可见性。
当且仅当满足以下所有条件时才应该使用volatile变量
1.对变量的写入操作不依赖变量的当前值,或者能确保只有单个线程更新变量值
2.该变量不会与其它状态变量一起纳入不变性条件当中
3.在访问变量时不需要锁(锁本身已经有了可见性,再使用volatile就很多余)

一个调试小提示

发布与溢出
核心
发布一个对象的意思是指使对象能够在当前作用域之外的代码中使用。
注意事项:
1.不要使内部的可变状态溢出(getter、setter,尽量对引用类型使用clone)
2.不要显示或者隐式的让this引用溢出
安全对象的构造过程
不要在构造过程当中使this引用溢出,特别是内部类对象。
构造方法应该只有简单逻辑,对于含有复杂逻辑的构造过程,为了安全构造对象和代码清晰,应该使用工厂方法来初始化
注意:
还要担心重排序问题,构造方法和引用赋值不满足先行发生原则,因此可能发生构造方法还未结束,但引用已经被获取到的情况,可以使用volatile和final变量来禁止重排序。

线程封闭
如果对象被封闭在线程内部,那么就没有任何的线程问题
1.Ad-hoc线程封闭
   感觉是废话,就是说维护线程封闭性的原则完全由程序实现,没有任何一种语言特性可以保证这个

2.栈封闭
   局部变量is ok

3.ThreadLocal类
   不多说,但莫乱用

不变性
不可变对象
 不可变对象一定是线程安全的
 不可变对象需要满足以下条件:
  1.对象创建以后,状态就不能修改
  2.对象的所有域都是final(可放宽)
  3.对象是正确创建的(对象创建期间,this引用没有溢出)
  注意:不可变对象与不可变的对象引用不是一回事!

Final域
  关键字final可以视为C++中const机制的一种受限版本,用于构造不可变性对象,final类型的域是不可修改的。然而在java内存模型当中,final域还有着特殊的语义(禁止final域赋值重排序到构造方法之外),从而能确保初始化过程的安全性(参考:http://blog.csdn.net/lovesummerforever/article/details/78620585
   对不可变的域都用final修饰是个好习惯

不可变的对象的一种替代同步方案的做法
   使用包含多个状态变量的容器来维持不变性条件,并使用一个volatile引用来保证可见性。

安全发布
不可变对象与初始化安全性
 主要还是构造方法内的代码和构造方法后的代码的重排序问题
 不可变对象任何时候都是线程安全的(发布时和使用时)
 不可变对象可以确保初始化安全性,其实volatile也可以!
  注意:重排序问题,构造方法和引用赋值不满足先行发生原则,因此可能发生构造方法还未结束,但引用已经被获取到的情况,可以使用volatile和final变量来禁止重排序
安全发布的常用模式
对于可变对象,可以使用以下方法来正确发布

事实不可变对象
事实不可变对象只要保证初始化安全(发布时),后面的使用就是安全的

可变对象
可变对象不只在对象发布时需要保证安全,使用时也需要保证安全

安全地共享对象
对象使用时的安全策略:


原文地址:http://www.infoq.com/cn/articles/java-memory-model-6

与前面介绍的锁和volatile相比较,对final域的读和写更像是普通的变量访问。对于final域,编译器和处理器要遵守两个重排序规则:
  1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
下面,我们通过一些示例性的代码来分别说明这两个规则:
public class FinalExample {
int i; //普通变量
final int j; //final变量
static FinalExample obj;

public void FinalExample () { //构造函数
i = 1; //写普通域
j = 2; //写final域
}

public static void writer () { //写线程A执行
obj = new FinalExample ();
}

public static void reader () { //读线程B执行
FinalExample object = obj; //读对象引用
int a = object.i; //读普通域
int b = object.j; //读final域
}
}
相关厂商内容
相关赞助商
这里假设一个线程A执行writer ()方法,随后另一个线程B执行reader ()方法。下面我们通过这两个线程的交互来说明这两个规则。

写final域的重排序规则

写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:
现在让我们分析writer ()方法。writer ()方法只包含一行代码:finalExample = new FinalExample ()。这行代码包含两个步骤:
  1. 构造一个FinalExample类型的对象;
  2. 把这个对象的引用赋值给引用变量obj。
假设线程B读对象引用与读对象的成员域之间没有重排序(马上会说明为什么需要这个假设),下图是一种可能的执行时序:
在上图中,写普通域的操作被编译器重排序到了构造函数之外,读线程B错误的读取了普通变量i初始化之前的值。而写final域的操作,被写final域的重排序规则“限定”在了构造函数之内,读线程B正确的读取了final变量初始化之后的值。
写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程B“看到”对象引用obj时,很可能obj对象还没有构造完成(对普通域i的写操作被重排序到构造函数外,此时初始值2还没有写入普通域i)。

读final域的重排序规则

读final域的重排序规则如下:
初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如alpha处理器),这个规则就是专门用来针对这种处理器。
reader()方法包含三个操作:
  1. 初次读引用变量obj;
  2. 初次读引用变量obj指向对象的普通域j。
  3. 初次读引用变量obj指向对象的final域i。
现在我们假设写线程A没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序:
在上图中,读对象的普通域的操作被处理器重排序到读对象引用之前。读普通域时,该域还没有被写线程A写入,这是一个错误的读取操作。而读final域的重排序规则会把读对象final域的操作“限定”在读对象引用之后,此时该final域已经被A线程初始化过了,这是一个正确的读取操作。
读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final域一定已经被A线程初始化过了。

如果final域是引用类型

上面我们看到的final域是基础数据类型,下面让我们看看如果final域是引用类型,将会有什么效果?
请看下列示例代码:
public class FinalReferenceExample {
final int[] intArray; //final是引用类型
static FinalReferenceExample obj;

public FinalReferenceExample () { //构造函数
intArray = new int[1]; //1
intArray[0] = 1; //2
}

public static void writerOne () { //写线程A执行
obj = new FinalReferenceExample (); //3
}

public static void writerTwo () { //写线程B执行
obj.intArray[0] = 2; //4
}

public static void reader () { //读线程C执行
if (obj != null) { //5
int temp1 = obj.intArray[0]; //6
}
}
}
这里final域为一个引用类型,它引用一个int型的数组对象。对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:
  1. 在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
对上面的示例程序,我们假设首先线程A执行writerOne()方法,执行完后线程B执行writerTwo()方法,执行完后线程C执行reader ()方法。下面是一种可能的线程执行时序:
在上图中,1是对final域的写入,2是对这个final域引用的对象的成员域的写入,3是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的1不能和3重排序外,2和3也不能重排序。
JMM可以确保读线程C至少能看到写线程A在构造函数中对final引用对象的成员域的写入。即C至少能看到数组下标0的值为1。而写线程B对数组元素的写入,读线程C可能看的到,也可能看不到。JMM不保证线程B的写入对读线程C可见,因为写线程B和读线程C之间存在数据竞争,此时的执行结果不可预知。
如果想要确保读线程C看到写线程B对数组元素的写入,写线程B和读线程C之间需要使用同步原语(lock或volatile)来确保内存可见性。

为什么final引用不能从构造函数内“逸出”

前面我们提到过,写final域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了。其实要得到这个效果,还需要一个保证:在构造函数内部,不能让这个被构造对象的引用为其他线程可见,也就是对象引用不能在构造函数中“逸出”。为了说明问题,让我们来看下面示例代码:
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;

public FinalReferenceEscapeExample () {
i = 1; //1写final域
obj = this; //2 this引用在此“逸出”
}

public static void writer() {
new FinalReferenceEscapeExample ();
}

public static void reader {
if (obj != null) { //3
int temp = obj.i; //4
}
}
}
假设一个线程A执行writer()方法,另一个线程B执行reader()方法。这里的操作2使得对象还未完成构造前就为线程B可见。即使这里的操作2是构造函数的最后一步,且即使在程序中操作2排在操作1后面,执行read()方法的线程仍然可能无法看到final域被初始化后的值,因为这里的操作1和操作2之间可能被重排序。实际的执行时序可能如下图所示:
从上图我们可以看出:在构造函数返回前,被构造对象的引用不能为其他线程可见,因为此时的final域可能还没有被初始化。在构造函数返回后,任意线程都将保证能看到final域正确初始化之后的值。

final语义在处理器中的实现

现在我们以x86处理器为例,说明final语义在处理器中的具体实现。
上面我们提到,写final域的重排序规则会要求译编器在final域的写之后,构造函数return之前,插入一个StoreStore障屏。读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。
由于x86处理器不会对写-写操作做重排序,所以在x86处理器中,写final域需要的StoreStore障屏会被省略掉。同样,由于x86处理器不会对存在间接依赖关系的操作做重排序,所以在x86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说在x86处理器中,final域的读/写不会插入任何内存屏障!

JSR-133为什么要增强final的语义

在旧的Java内存模型中 ,最严重的一个缺陷就是线程可能看到final域的值会改变。比如,一个线程当前看到一个整形final域的值为0(还未初始化之前的默认值),过一段时间之后这个线程再去读这个final域的值时,却发现值变为了1(被某个线程初始化之后的值)。最常见的例子就是在旧的Java内存模型中,String的值可能会改变(参考文献2中有一个具体的例子,感兴趣的读者可以自行参考,这里就不赘述了)。
为了修补这个漏洞,JSR-133专家组增强了final的语义。通过为final域增加写和读重排序规则,可以为java程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指lock和volatile的使用),就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值。

参考文献

  1.  Java Concurrency in Practice
  2.  JSR 133 (Java Memory Model) FAQ
  3.  Java Concurrency in Practice
  4.  The JSR-133 Cookbook for Compiler Writers
Intel® 64 and IA-32 ArchitecturesvSoftware Developer’s Manual Volume 3A: System Programming Guide, Part 1

双重检查锁的一般套路:
public class DoubleCheckedLocking {

    private static volatile Object instance;

    private static final Object innerLocker = new Object();

    public static Object getInstance(){
        if(instance == null){
            synchronized (innerLocker){
                if(instance == null){
                    instance = new Object();
                }
            }
        }
        return instance;
    }
}

双重检查锁的核心是两重null检查和volatile修饰符!这两个是必需的条件。

虽然同步块已经保证了instance的可见性,但是还存在重排序问题。

volatile可以通过cpu指令加个内存屏障来放置乱序执行,从而保证对象在正确构造以后才会将引用赋值instance,否则其它线程可能会拿到一个尚未初始化完成的对象。

注意:
volatile的语义在1.5才被修复,因此1.5之前的volatile无法保证可见性和有序性,DLC是无法保证安全的!
x86处理器不会做上面的乱序执行,因此x86处理器上 DLC是安全的,不过程序不能因为硬件平台而存侥幸!要绝对安全!

参考文章:
win10-x64系统编译OpenJDK:http://blog.csdn.net/tangyongzhe/article/details/53576097
Java虚拟机原理分析之Win7下VS2010编译OpenJDK8与单步调试HotSpot VM过程详细记录:http://blog.csdn.net/lpwstr/article/details/78840188
Java虚拟机原理分析之Win10下VS2017编译OpenJDK8与单步调试HotSpot VM过程详细记录:http://blog.csdn.net/LPWSTR/article/details/78849587



一.设计线程安全的类
    使用封装技术更能确保线程安全
    设计线程安全类的过程,需要包含以下三个基本要素:
  1. 找出构成对象状态的所有变量
  2. 找出约束状态变量的不变性条件
  3. 建立对象状态的并发访问管理策略
     1.收集同步需求
        如果不了解对象的不变性条件、后验条件那么就不能确保线程安全性。要满足在状态变量的有效值(不变性条件)或状态转换(后验条件)上的各种约束条件,就需要借助原子性与封装性(或者其它同步策略)
     2.依赖状态的操作
        类的不变性条件与后验条件约束了在对象上有哪些状态或者状态转换是有效的,但是在对象的方法中还包含一些基于状态的先验条件
     3.状态的所有权
       一般来说,对象封装它所拥有的状态,其同时也拥有状态的所有权和控制权,对象需要保证状态的安全。
       对于一些容器应用,不一定是这种情况(servletcontext)
二.实例封闭
    将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
    封闭机制更易于构造线程安全的类,因为当封闭类的状态时,分析类的线程安全性时就无须检查整个程序。
    1.java监视器模式
      其实就是使用synchronized(底层是monitorenter、monitorexit字节码指令),算不上一个什么模式。比如hashtable、vector、Collections.synchronizedXXX方法等实现。


三.线程安全性的委托
   1.独立的状态变量
     CopyOnWriteArrayList特别适合拿来做监听器列表(读多写少
   
  2.当委托失效时
     如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。
     但是如果状态变量不独立,那么即使自身操作是安全的,整个都不是安全的。

  3.发布底层的状态变量
     如果一个状态变量是线程安全的,并且没有任何不可变性条件来约束它的值,在变量的操作上也不存在任何不准许的状态转换,那么就可以安全发布这个变量。

四.在现有的线程安全类中添加功能
    1.继承-派生:必须了解类内部的线程安全策略,无法向后兼容。
    2.客户端加锁:必须了解类内部的线程安全策略,而且容易出错,且无法向后兼容。
    3.组合(委托):装饰器模式,类似Collections.synchronizedXXX方法,在外部加一层锁。perfect!

五.将同步策略文档化
    在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。
    几个jcip的注解可以帮忙



     



  • 摩尔定律:集成电路芯片上所集成的晶体管数量,越每隔18个月便会翻一番(实际上是指可以通过提高CPU时钟频率来提高程序运行速度,但多于现在的高并发程序,该定律无法带来实质性的解决,且CPU时钟频率提高速度越来越慢)。

  • Amdahl定律:对计算机系统的某个部件采用优化措施后所获得的计算机性能的提高,依赖于这部分的执行时间在整个运行时间中所占的比率。程序的运行时间=串行运行时间+可并行运行时间/cpu核数,该公式实际上是现代并发程序的指导原则,多任务处理!

  • 竞争条件:多个任务并发访问和操作同一数据且执行结果与访问的特定顺序有关,称为竞争条件。(多个任务竞争响应某个条件,因访问顺序不同产生冲突或不一致的情况)。比如:“先检查后运行“、“取值-运行-写入”、“惰性初始化”。

  • 数据竞争:这个概念经常会和竞态条件相混淆,数据竞争指的是两条或两条以上的线程并发地访问同一块内存区域,同时其中至少有一条是为了写,而且这些线程没有协调对那块内存区域的访问,当满足这些条件的时候,访问顺序就是不确定的。通俗的说写和读的线程可能因为指令被重排序、可见性不够等原因拿到一个不正确的数据。比如:失效数据、非正确初始化。

  • 重排序:编译器可能会为了优化程序,在编译器将程序的指令重排序,从而达到利用寄存器来临时存储变量的作用。CPU执行指令为了优化指令,将指令乱序执行来提升效率和利用CPU高速缓存。 

  • 原子操作:任务在执行过程中不能被打断且不可再分割的一序列操作,通俗的说,如果A线程执行一个操作,这个操作在另外的线程某时刻看起来要么是未执行,要么是已经执行完,那这个操作就是原子的。

  • 复合操作:任务在执行过程中可以被打断且可再分割的一序列操作。

  • 不变约束:核心是不变式,不变式表达了对状态的约束,这些状态是应该符合这个约束的值的组合,代表某种规则,比如我们要求一个字段只能为正整型。

  • 先验条件:针对方法,规定了在条用方法之前必须为真的条件。比如向一个有界队列写数据要求队列不能已满。

  • 后验条件:针对方法,规定了在条用方法之后必须为真的条件。比如自增一个int,要求增加确确实实起到了+1的作用。

  • 原子性:核心就是原子操作

  • 可见性:确保线程对变量的写入对其他线程是可见的。

  • 有序性:使用内存屏障,保证保证线程之间指令操作的顺序,一定成效的避免重排序。

  • 线程安全:类的方法可以安全在多线程当中使用而不会产生错误(相对线程安全最普遍,其要求的是单个方法调用安全)

  • 活跃性:线程能否运行下去,避免死锁、活锁、饥饿问题

  • 性能问题:主要偏向于程序的行为正确,但是却无法满足性能需求的问题

  •  对象发布的安全性:对象初始化的安全性,防止初始化期间this引用溢出和重排序引起的问题

  • 对象共享的安全性:对象在多个线程中并发使用的安全性


一.同步容器类
    同步容器类包括vector、hashtable、Collections.synchronizedXXX工厂方法装饰的容器
    1.同步容器类的问题
      同步容器类都是相对线程安全的,但在某些情况下可能需要额外的客户端加锁(或者派生和组合)来保护复合操作。
      常见的同步容器类的复合操作:迭代、跳转、条件运算
    2.迭代器与ConcurrentModificationException
     除了copyonwriteXXX,其它的容器基本都是fail-fast,迭代期间有另外线程修改容器,或者自身迭代时修改容器,就会抛出ConcurrentModificationException
     这种策略叫做“fail-fast”,及时失败策略
    3.隐藏迭代器
     正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施同步策略。
     容器的toString、hashcode、toString、containsAll、removeAll、retainAll以及把容器作为参数的构造方法,大部分都会对容器进行迭代。

二.并发容器
            java5.0提供了多种并发容器来改进同步容器的性能。同步容器将所有对容器状态的访问都串行化来实现线程安全性。这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重降低。
    另一方面,并发容器是针对多个线程并发访问设计的。
           通过并发容器来替代同步容器,可以极大的提高伸缩性并降低风险。
           并发容器包括:
           1.ConcurrentHashMap(还有ConcurrentHastSet)用来替代同步且基于散列的HashTable,新的CurrentMap接口中增加了对一些常见复合操作的支持,例如“若没有则添加”、“替换以及有条件删除”等。
           2.CopyOnWriteArrayList用于在遍历操作为主要操作的情况下替换vector
           3.增加了队列接口queue和并发实现ConcurrentLinkedQueue
           4.增加了双端队列接口Deque和并发实现ConcurrentLinkedDeque
           5.java6 还增加了阻塞队列接口BlockingQueue(扩展了queue)和其的相关实现:LinkedBlockingQueue,ArrayBlockingQueue
           6.java6 还增加了双端阻塞队列接口BlockingDeque(扩展了BlockingQueue )和其的相关实现:LinkedBlockingDeque
           7.增加了优先级阻塞队列PriorityBlockingQueue
           8.java6还提供了ConcurrentSkipListMap和ConcurrentSkipListSet(基于跳表)分别作为替换SortedMap和SortedSet的并发替代品
         
       1.ConcurrentHashMap
          java7及之前使用分段锁,java8使用node锁
          迭代器不会抛出ConcurrentModifyException,是fail-safe,提供的是弱一致性。
          更高的伸缩性!优先使用
       2.额外的原子Map操作
         由于并发容器的内部实现都不是简单的加锁来保护,含有比较复杂的逻辑,因此对于一些并发容器没有的复合操作,不能通过外部加锁来保证安全。
         所以并发容器提供了一些普通容器没有的复合操作。
       3.CopyOnWriteArrayList
        用于替代同步list,在某些情况下它提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁或者复制。
        写入时复制容器的线程安全性在于:只要正确发布一个事实不可变的对象,那么在访问该对象时都不需要进一步的同步,在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变现。
        写与写还是互斥的,使用的是显式锁,很经典的设计!空间换时间。
         仅当读远多于写时,才能使用这种容器。
三.阻塞队列和生产者-消费者模式
      阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。
      阻塞队列支持生产者-消费者这种设计模式,生产者-消费者模式能简化开发过程,因为它消除了生产者代码和消费者代码之间的代码依赖,此外还将生产数据的过程与使用数据的过程解耦开来以简化工作之间负载的管理,因为往往这两个过程在处理数据的速率有所不同。
      Executor实际上就有生产者-消费者的思想。
     无界阻塞队列的缺陷:如果生产者生产速度远大于消费者的消费速度,那么工作项就会在队列累积并最终耗尽内存,使用有界队列可以避免这个问题,这是个用时间换空间的策略。
     阻塞队列还提供了一个offer方法,如果数据项不能添加到队列当中,那么将返回false,这样就能创建更多灵活的策略来处理负荷过载的情况,比如减轻负载、将多余工作序列化到磁盘,减少生产者数量或者抑制生产者线程等。
     在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能移植并防止产生过多的工作项,使应用程序在负荷过载的情况下更加健壮。
     一定要提前考虑生产者速率比消费者速率快的问题!
    SynchronousQueue提供了一个没有存储功能的阻塞队列,put和take会持续阻塞,直到有生产者生产数据&消费者消费数据才会运行下去(类似于go中的通道),只有在有很多消费者,并且每次都有消费者准备处理数据才适合这种。
    1.示例 桌面搜索
       生产者-消费者模式当中,阻塞队列可以负责控制流,生产者和消费者完全解耦,代码逻辑清晰!
       生产者-消费者模式的性能优势:生产者和消费者可以并发执行,如果一个是I/0密集型,一个是CPU密集型,那么并发执行的吞吐率要高于串行执行的吞吐率,cpu密集型:I/O密集型=n : m
                                                       如果生产者和消费者的速率不同,那么它们将紧密耦合在一起,从而把整体的并行度降低为二者中更小的并行度。
    2.串行线程封闭
       线程封闭对象只能单个线程拥有,但可以通过安全地发布该对象来转移所有权,并且发布对象的线程不会再访问它。
       阻塞队列内部都包括了足够的同步机制,可以安全地将对象从生产者线程发布到消费者线程
       对于可变对象,生产者-消费者这种设计与阻塞队列一起,促进了串行线程封闭,从而将对象所有权从生产者交互给消费者。这种安全的发布确保了对象状态对于新的所有者来说是可见的,并且由于最初的所有者不会再拥有它,因此对象被封闭在新的线程中。新的所有者线程可以对该对象做任意修改,因为它具有独占的访问权。
      对象池(连接池、资源池)充分利用了串行线程封闭(这儿不是使用的阻塞队列来做的)
      也可以使用其它发布机制来传递对象的所有权,但必须确保只有一个线程能接受被转移的对象。阻塞队列简化了这项工作。除此之外,还可以通过ConcurrentMap的原子方法remove或者AtomicReference的原子方法compareAndSet来完成这项工作。
    3.双端队列与工作窃取
       双端队列适用于工作窃取模式,
      工具窃取设计当中,如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。工作窃取模式比生产者-消费者模式具有更高的伸缩性和性能,因为工作者线程不会在单个共享的任务队列上发生竞争并且可以充分利用线程资源。
      工作窃取适用于既是消费者,又是生产者的问题——执行某个工作,可能产生更多的工作。比如:网页爬虫、垃圾回收标记。
四.阻塞方法和中断方法
    当线程被阻塞时,它通常被挂起,并处于某种阻塞状态(BLOCKED、WAITING或TIMED_WATING),被阻塞的线程必须等待某个不受它控制的事件发生后才能继续运行。
    Thread提供了interrupt方法用于中断线程,interrupted方法用于查询线程是否中断(如果处于中断会抹掉中断标志)。每个线程都有一个布尔类型的属性,表示线程的中断状态,当中断线程时将设置这个状态。
    中断是一种协作机制。一个线程不能强制要求其它线程停止正在执行的操作而去执行其它的操作。当线程A中断线程B时,A仅仅是要求B在执行到某个可以暂停的地方停止正在执行的操作(但不会停止线程)——前提是线程B愿意停下来。
    当代码中调用了一个会抛出InterrupedException的方法时,必须要处理处理中断响应,不能只catch而不做处理。对于库代码,主要有以下几种选择:
   1.传递InterrupedException
   2.恢复中断(调用interrupt方法)
    对于用户代码还可以直接处理中断!但绝对不能捕获它而不做响应。
五.同步工具类
    同步工具类可以是任何一个对象,只要它根据自身的状态来协调线程的控制流。阻塞队列可以作为线程工具类,其它类型的同步工具还包括信号量(Semaphore)、栅栏(Barrier&Exchanger)以及闭锁(Latch)等。所有同步工具类都包含一些特定的结构化属性:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及另外一些高效方法用于高效地等待同步工具进入到预期状态。
    1.闭锁(CountDownLatch)
      闭锁的作用:
      (1)确保某个计算在其所有需要资源都被初始化才进行。
      (2)确保服务在其依赖的所有服务都已经启动后才启动。
      (3)等待直到某个操作所有的参与者都就绪再执行。
    2.FutureTask
       FutureTask也可以用作闭锁。
       
   3.信号量(Semphore)
   信号量可以用于实现资源池或者将任一容器变为有界阻塞容器。

  4.栅栏(CyclicBarrier&Exchanger)
   
六.构建高效且可伸缩的结果缓存
    四个代码,逐渐加深优化,好好体会

第一部分(基础知识)小结
1.可变状态是至关重要的。所有并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性。
2.尽量将域声明为final类型,除非需要它们是可变的。
3.不可变对象一定线程安全。
4.封装有助于管理复杂性:在编写线程安全的类时,虽然可以将所有数据都保存在全局变量中,但为什么要这样做?将数据封装在对象中,更易于维持不变性条件,将同步机制封装在类中,更易于遵守同步策略。
5.用锁来保护每个可变变量。
6.当保护一个不变性条件中的所有变量时,使用同一个锁。
7.在执行复合操作期间,要持有锁。
8.如果多个线程访问一个可变变量时没有同步机制,那么程序会有问题。
9.不要故作聪明不使用同步。
10.在设计过程中考虑线程安全,或者在文档中明确指出它不是线程安全的。
11.将同步策略文档化。
 






生产者-消费者模式是一种很优秀的架构设计模式,将程序分割为生产部分和消费部分,前者生产待处理任务,后者处理任务,中间的纽带可以通过一些相关容器(大的说就是消息队列服务器,小的说比如阻塞队列)
阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。
      阻塞队列支持生产者-消费者这种设计模式,生产者-消费者模式能简化开发过程,因为它消除了生产者代码和消费者代码之间的代码依赖,此外还将生产数据的过程与使用数据的过程解耦开来以简化工作之间负载的管理,因为往往这两个过程在处理数据的速率有所不同。
      Executor实际上就有生产者-消费者的思想。
     无界阻塞队列的缺陷:如果生产者生产速度远大于消费者的消费速度,那么工作项就会在队列累积并最终耗尽内存,使用有界队列可以避免这个问题,这是个用时间换空间的策略。
     阻塞队列还提供了一个offer方法,如果数据项不能添加到队列当中,那么将返回false,这样就能创建更多灵活的策略来处理负荷过载的情况,比如减轻负载、将多余工作序列化到磁盘,减少生产者数量或者抑制生产者线程等。
     在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能移植并防止产生过多的工作项,使应用程序在负荷过载的情况下更加健壮。
     一定要提前考虑生产者速率比消费者速率快的问题!
    SynchronousQueue提供了一个没有存储功能的阻塞队列,put和take会持续阻塞,直到有生产者生产数据&消费者消费数据才会运行下去(类似于go中的通道),只有在有很多消费者,并且每次都有消费者准备处理数据才适合这种。
    阻塞队列的两个重要作用:
    1.提高建模的简单些和运行性能
       生产者-消费者模式当中,阻塞队列可以负责控制流,生产者和消费者完全解耦,代码逻辑清晰!
       生产者-消费者模式的性能优势:生产者和消费者可以并发执行,如果一个是I/0密集型,一个是CPU密集型,那么并发执行的吞吐率要高于串行执行的吞吐率,cpu密集型:I/O密集型=n : m
                                                       如果生产者和消费者的速率不同,那么它们将紧密耦合在一起,从而把整体的并行度降低为二者中更小的并行度。
    2.串行线程封闭
       线程封闭对象只能单个线程拥有,但可以通过安全地发布该对象来转移所有权,并且发布对象的线程不会再访问它。
       阻塞队列内部都包括了足够的同步机制,可以安全地将对象从生产者线程发布到消费者线程
       对于可变对象,生产者-消费者这种设计与阻塞队列一起,促进了串行线程封闭,从而将对象所有权从生产者交互给消费者。这种安全的发布确保了对象状态对于新的所有者来说是可见的,并且由于最初的所有者不会再拥有它,因此对象被封闭在新的线程中。新的所有者线程可以对该对象做任意修改,因为它具有独占的访问权。
      对象池(连接池、资源池)充分利用了串行线程封闭(这儿不是使用的阻塞队列来做的)
      也可以使用其它发布机制来传递对象的所有权,但必须确保只有一个线程能接受被转移的对象。阻塞队列简化了这项工作。除此之外,还可以通过ConcurrentMap的原子方法remove或者AtomicReference的原子方法compareAndSet来完成这项工作。

什么是串行线程封闭?
看上去是在多个线程之间共享的对象,实际上每次只有一个线程去使用它,这就保证了对象是被线程串行访问的,只要保证了对象在线程之间所有权的转移,那么这个对象就是线程安全的。
线程封闭对象只能单个线程拥有,但可以通过安全地发布该对象来转移所有权,并且发布对象的线程不会再访问它。
怎么保证串行线程封闭?
1.阻塞队列
 阻塞队列内部都包括了足够的同步机制,可以安全地将对象从生产者线程发布到消费者线程
 对于可变对象,生产者-消费者这种设计与阻塞队列一起,促进了串行线程封闭,从而将对象所有权从生产者交互给消费者。这种安全的发布确保了对象状态对于新的所有者来说是可见的,并且由于最初的所有者不会再拥有它,因此对象被封闭在新的线程中。新的所有者线程可以对该对象做任意修改,因为它具有独占的访问权。
 2.池化技术
  对象池(连接池、资源池)充分利用了串行线程封闭
 3.其它技术
  可以通过ConcurrentMap的原子方法remove或者AtomicReference的原子方法compareAndSet来完成这项工作。

      双端队列适用于工作窃取模式,
      工作窃取设计当中,如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。
      工作窃取模式比生产者-消费者模式具有更高的伸缩性和性能,因为工作者线程不会在单个共享的任务队列上发生竞争并且可以充分利用线程资源。
      工作窃取适用于既是消费者,又是生产者的问题——执行某个工作,可能产生更多的工作。
      适用案例:
      1.网页爬虫
      2.垃圾回收标记
      3.fork-join pool

强制停止一个线程不是安全的,因为不可控,可能会给应用带来数据不一致的危险。。。。。
线程中断机制是一个非常棒的机制!
当线程被阻塞时,它通常被挂起,并处于某种阻塞状态(BLOCKED、WAITING或TIMED_WATING),被阻塞的线程必须等待某个不受它控制的事件发生后才能继续运行。
    Thread提供了interrupt方法用于中断线程,interrupted方法用于查询线程是否中断(如果处于中断会抹掉中断标志)。每个线程都有一个布尔类型的属性,表示线程的中断状态,当中断线程时将设置这个状态。
    中断是一种协作机制。一个线程不能强制要求其它线程停止正在执行的操作而去执行其它的操作。当线程A中断线程B时,A仅仅是要求B在执行到某个可以暂停的地方停止正在执行的操作(但不会停止线程)——前提是线程B愿意停下来。
    当代码中调用了一个会抛出InterrupedException的方法时,必须要处理处理中断响应,不能只catch而不做处理。对于库代码,主要有以下几种选择:
   1.传递InterrupedException
   2.恢复中断(调用interrupt方法)
    对于用户代码还可以直接处理中断!但绝对不能捕获它而不做响应。

英语中字教程: https://www.bilibili.com/video/av19349170/
官网:https://www.bilibili.com/video/av19349170/

官网地址:https://glot.io/


java.version
Java 运行时环境版本
java.vendor
Java 运行时环境供应商
java.vendor.url
Java 供应商的 URL
java.home
Java 安装目录
java.vm.specification.version
Java 虚拟机规范版本
java.vm.specification.vendor
Java 虚拟机规范供应商
java.vm.specification.name
Java 虚拟机规范名称
java.vm.version
Java 虚拟机实现版本
java.vm.vendor
Java 虚拟机实现供应商
java.vm.name
Java 虚拟机实现名称
java.specification.version
Java 运行时环境规范版本
java.specification.vendor
Java 运行时环境规范供应商
java.specification.name
Java 运行时环境规范名称
java.class.version
Java 类格式版本号
java.class.path
Java 类路径
java.library.path
加载库时搜索的路径列表
java.io.tmpdir
默认的临时文件路径
要使用的 JIT 编译器的名称
java.ext.dirs
一个或多个扩展目录的路径
os.name
操作系统的名称
os.arch
操作系统的架构
os.version
操作系统的版本
file.separator
文件分隔符(在 UNIX 系统中是“/”)
path.separator
路径分隔符(在 UNIX 系统中是“:”)
line.separator
行分隔符(在 UNIX 系统中是“/n”)
user.name
用户的账户名称
user.home
用户的主目录
user.dir
用户的当前工作目录



快照
什么是快照模式
快照模式是在遍历对象的各项数据(状态)时,遍历前返回一个快照对象(deepclone对象,clone时有必要需要保证线程安全),这个快照对象和本对象没有任何的联系,这样在遍历时就不会有任何的线程问题(虽然无法获取实时数据,但是数据状态是完整的和正确的) ,集合也不会抛出ConcurrentModifyException),且在一定程度上能够提高效率。
参考代码
/**
      * 快照模式
      *
      * @author HuaRanFan
      *
      */
     private static class Wraper implements Iterable<Integer> {
          private final HashSet<Integer> set = new HashSet<Integer>();
          private final Object lock = new Object();
          public boolean add(Integer item) {
              synchronized (lock) {
                   return set.add(item);
              }
          }
          public boolean remove(Integer item) {
              synchronized (lock) {
                   return set.remove(item);
              }
          }
          /************* 略 **************/
          private Set<Integer> getItems() {
              // 快照核心 遍历时返回一个快照对象,注意要保证复制时的线程安全性
              // 必要时需要加锁
              synchronized (lock) {
                   return Collections.unmodifiableSet((Set<Integer>) set.clone());
              }
          }
          @Override
          public Iterator<Integer> iterator() {
              return getItems().iterator();
          }
     }
适用场景
1.写远多于读,且存在遍历的情况
2.遍历时,每个元素的处理时间稍长
3.只要求弱一致性,不要求实时性
版本化
什么是版本化模式
版本化模式是适用volatile来修饰容器,从而保证容器引用的可见性 ,读和遍历数据不用加任何的锁, 在修改容器时,先持有锁,然后复制一个容器(浅拷贝,元素安全性自我保证),然后再新容器上进行修,随后更新容器引用,再释放锁,这样虽然会加大内存开销,但在一定程度上能够提高读数据效率,因为每次都是新容器,所以这种模式叫做版本化,版本化在遍历时也是弱一致性,非实时。
参考代码
/**CopyOnWriteArrayList**/
    public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;
    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();
    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
    
    / Positional Access Operations
    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

      /**
     * Replaces the element at the specified position in this list with the
     * specified element.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);
            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
适用场景
1.读远多于写
2.遍历只要求弱一致性,不要求实时性
3.使用场景很广,数据库、地图图层等等。

快照和版本化的联系
快照和版本化都是基于空间换时间的策略,版本化是写的时候复制 ,快照是读的时候复制。


自我总结
1.Object.Clone不是线程安全的方法,但并不一定对象在多线程共享时使用Clone就一定要加锁,要看具体需求。
2.使用快照模式,Clone一定要使用深克隆

参考文章

Stackoverflow
https://stackoverflow.com/questions/27440503/is-super-clone-of-cloneable-a-thread-safe-method
You are slightly misusing the term "thread-safe". It does not mean "synchronized", and that's apparently how you are using it. No amount of synchronization can prevent implementation errors from breaking thread safety. As an example, any code you wrote which mutates the object while not holding any lock will clearly violate thread safety of the object, and there is nothing a library method like Object.clone can do about that.
Ultimately, thread safety is always in the hands of the implementor and Object.clone() will not do anything to make that harder for you: all it does is read the state of the current object and copy it to the new object. It does not publish that new object.
clone is not specifically described as thread-safe, which means it's not. If one thread is cloning the object while another thread is changing it, the clone can end up in an inconsistent state.
You could grab a lock in your clone function, but much better would be to grab it in the code which calls clone.


Effective java
http://kubicode.me/2015/05/03/Java%20Base/You-Donnot-Kown-About-The-Clone/

clone() 元定义

啥都不说,先看下源码中Object.clone()的定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* Creates and returns a copy of this object. The precise meaning
* of "copy" may depend on the class of the object. The general
* intent is that, for any object {@code x}, the expression:
* <blockquote>
* <pre>
* x.clone() != x</pre></blockquote>
* will be true, and that the expression:
* <blockquote>
* <pre>
* x.clone().getClass() == x.getClass()</pre></blockquote>
* will be {@code true}, but these are not absolute requirements.
* While it is typically the case that:
* <blockquote>
* <pre>
* x.clone().equals(x)</pre></blockquote>
* will be {@code true}, this is not an absolute requirement.
* <p>
* By convention, the returned object should be obtained by calling
* {@code super.clone}. If a class and all of its superclasses (except
* {@code Object}) obey this convention, it will be the case that
* {@code x.clone().getClass() == x.getClass()}.
* 创建和返回对象的拷贝需要满足
* x.clone()!=x (拷贝返回的东西不能用原来的地址啊~^_^)
* x.clone().getClass==x.getClass() (这个不是绝对的,可以返回子类)
* x.clone().equals(x) (拷贝了之后当然两个对象时相等的~,这个也不是绝对的,比如有序 列化唯一ID的字段)
*
* <p>
* By convention, the object returned by this method should be independent
* of this object (which is being cloned). To achieve this independence,
* it may be necessary to modify one or more fields of the object returned
* by {@code super.clone} before returning it. Typically, this means
* copying any mutable objects that comprise the internal "deep structure"
* of the object being cloned and replacing the references to these
* objects with references to the copies. If a class contains only
* primitive fields or references to immutable objects, then it is usually
* the case that no fields in the object returned by {@code super.clone}
* need to be modified.
* 教我们实现clone方法的时候先调用super.clone(),克隆出的对象上添加自己当前类所需要的元素
* <p>
* The method {@code clone} for class {@code Object} performs a
* specific cloning operation. First, if the class of this object does
* not implement the interface {@code Cloneable}, then a
* {@code CloneNotSupportedException} is thrown. Note that all arrays
* are considered to implement the interface {@code Cloneable} and that
* the return type of the {@code clone} method of an array type {@code T[]}
* is {@code T[]} where T is any reference or primitive type.
* Otherwise, this method creates a new instance of the class of this
* object and initializes all its fields with exactly the contents of
* the corresponding fields of this object, as if by assignment; the
* contents of the fields are not themselves cloned. Thus, this method
* performs a "shallow copy" of this object, not a "deep copy" operation.
* 如果对象没有实现Cloneable接口而又调用了super.clone 就是抛出CloneNotSupportedException异常
* 默认所有的数组都是继承了Cloneable接口的,它返回的是该数组类型的数组(T[] -_-),但是 *拷贝的时候直接是使用了那么些类型的对象,并没有使用它们的拷贝,所以数据的拷贝只是一
* 个“浅拷贝”
* <p>
* The class {@code Object} does not itself implement the interface
* {@code Cloneable}, so calling the {@code clone} method on an object
* whose class is {@code Object} will result in throwing an
* exception at run time.
* Object类并没有实现Cloneable接口,所以你如果调用了Oject.clone方法话就会抛出异常,
* (但是在在这里可以看到Object的clone方法是protected的,该方法是无法直接调用的,除非
* 你使用反射来进行调用)
*
* @return a clone of this instance.
* @exception CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see java.lang.Cloneable
*/
protected native Object clone() throws CloneNotSupportedException;
Cloneable接口并不含任何方法,但是实现它的时候Objectclone方法就会返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常
注意,在java1.5以后的版本中clone()引入了协变返回类型,额可以直接直接支持指定类的返回,不必必须返回Object

clone不伤害源对象

clone()的原则就是必须确保不会伤害到原始的对象,并确保正确地创建被克隆中的约束条件。
实现克隆最基本的方法是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class CloneTest implements Cloneable{
public int[] elements=new int[10];
/**
* 进行数组的打印
*/
public void print()
{
System.out.println("\r\nprint");
for(int i=0;i<this.elements.length;i++)
{
System.out.print(elements[i]+"#");
}
}
@Override
public CloneTest clone()
{
CloneTest ret=null;
try
{
ret=(CloneTest)super.clone();//调用超类的clone
}catch(CloneNotSupportedException cs)
{
cs.printStackTrace();
}
return ret;
}
}
但是他会破坏原有对象:
1
2
3
4
5
6
7
8
9
CloneTest t1=new CloneTest();
t1.elements[0]=4;
t1.elements[1]=5;

CloneTest t2=t1.clone();
t2.elements[1]=6;

t1.print();
t2.print();
返回的是:
print4#6#0#0#0#0#0#0#0#0#print4#6#0#0#0#0#0#0#0#0#
这是因为该方法仅仅只是调用了超类的克隆方法,完了之后克隆出来的对象的elements元素还是与原对象一样。
所以一般在编写clone()之后,再在克隆出的对象上添加自己当前类所需要的元素,上述可以这么修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public CloneTest clone()
{
CloneTest ret=null;
try
{
ret=(CloneTest)super.clone();
ret.elements=this.elements.clone();//复制出来一个新的副本
}catch(CloneNotSupportedException cs)
{
cs.printStackTrace();
}
return ret;
}
在先前的测试代码之后得到的是
print4#5#0#0#0#0#0#0#0#0#print4#6#0#0#0#0#0#0#0#0#
简而言之,所有实现了Cloneable的类都应该调用一个共有的方法覆盖clone,此公有的方法首先调用super.clone(),然后修正任何需要修正的域(其实就是对当前自己类上的元素单独clone())。

数组的clone

引用的一个类
1
2
3
4
5
6
7
static class A{
public String name="";
public A(String name){
this.name=name;
}
}
对数组进行clone()
1
2
3
4
5
6
7
8
A[] a=new A[2];
a[0]=new A("tom");
a[0]=new A("peter");

A[] b=a.clone();
b[0].name="lili";、//修改克隆对象的值

System.out.println(a[0].name);
最终的输出为
lili
可以发现原有数组A的数据被他克隆出来的数组给修掉了,所以数组在执行clone()时并没有将数据类型的对象进行克隆,只是使用了它,这里也就是一个浅复制。(对于这种情况我们特别要注意~)

使用循环代替递归拷贝

HashTable中是使用一个列表数组来存储具体数据,在对HashTable进行拷贝时他的源码是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Creates a shallow copy of this hashtable. All the structure of the
* hashtable itself is copied, but the keys and values are not cloned.
* This is a relatively expensive operation.
*
* @return a clone of the hashtable
*/
public synchronized Object clone() {
try {
Hashtable<K,V> t = (Hashtable<K,V>) super.clone();
t.table = new Entry[table.length];
for (int i = table.length ; i-- > 0 ; ) {
t.table[i] = (table[i] != null)
? (Entry<K,V>) table[i].clone() : null;//这里调用Entry的clone方法
}
t.keySet = null;
t.entrySet = null;
t.values = null;
t.modCount = 0;
return t;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError();
}
}
接下来看下Entry.clone()方法:
1
2
3
4
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
从它的方法中我们可以看出来他是使用递归来进行调用的,不断进行递归执行自己的clone时自己的链表增长来满足clone,这可以说是一种比较简洁的方法,但是问题是出在递归,我们都知道执行递归方法时如果递归太深可能会触发虚拟机中最大stack数量的阈值导致抛StackOverflow的异常,并且执行一个方法本身也是一个比较耗资源的的操作,所以如果遇到这种情况可以考虑使用循环来完成这种需求。
1
2
3
4
5
6
7
8
9
10
//使用循环来代替递归
protected Object clone() {
Entry result=new Entry(key,value,next);
for(Entry p=result;p.next!=null;p=p.next)
{
p.next=new Entry(p.next.key,p.next.value,p.next.next);
}
return result;
}
针对此我们可以看下jdk1.2之后出来的HashMap里面的clone方法,他在super.clone()完了之后调用inflateTable()方法重新初始化了一个数组,然后再调用putAllForCreate()方法用循环的方式将现有数据进行添加进去完成clone()

可以考虑使用拷贝构造器来代码clone方法

该功能的类的拷贝构造器相比clone()方法来说有以下几个好处:
在该构造器可以有参数
支持final类型定义的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CloneTest{
public int[] elements=new int[10];

/**
* 进行数组的打印
*/
public void print()
{
System.out.println("\r\nprint");
for(int i=0;i<this.elements.length;i++)
{
System.out.print(elements[i]+"#");
}
}
public CloneTest()
{}
public CloneTest(CloneTest test){
CloneTest ret=test;
ret.elements=test.elements.clone();
}
}
这样的话就可以使用构造函数来进行对象的:
1
2
3
4
5
6
7
8
9
CloneTest t1=new CloneTest();
t1.elements[0]=4;
t1.elements[1]=5;

CloneTest t2=new CloneTest(t1);
t2.elements[1]=6;

t1.print();
t2.print();

总结

最后,关于clone()方法有以下几个注意点:
将出现递归clone()的时候尽量使用循环迭代来代替。
如果是线程安全的类要实现clone(),那些这个clone()也必须进行同步。
关于自身域的修正如果是遇到final类型的,那么这两者是不兼容的。
另一个实现对象拷贝的方式就是提供一个拷贝构造器,该构造器比clone()方法的一个优势就是它可以传参数。
当前类没有继承Cloneable接口时,如果到clone()方法里面调用了super.clone()方法就是抛出CloneNotSupportedException异常。

参考

《Effective Java中文版》第11条


java agent基础原理:http://blog.csdn.net/ancinsdn/article/details/58276945#

     构建Java Agent,而不是使用框架:http://www.importnew.com/15768.html



个人理解

原文
地址:http://mp.weixin.qq.com/s?__biz=MzU0OTE4MzYzMw==&mid=2247484493&idx=1&sn=1f4433d08a42e8ea53a1c99545a57489&chksm=fbb28db3ccc504a5cb1d68f320865c62c0159574d65004e32912e80c61d2319d25d78fb07564&mpshare=1&scene=23&srcid=0224Maz4s6stgNgFihBAgFAW#rd
1 分层架构
分层架构是最常见的架构,也被称为n层架构。多年以来,许多企业和公司都在他们的项目中使用这种架构,它已经几乎成为事实标准,因此被大多数架构师、开发者和软件设计者所熟知。
分层架构中的层次和组件是水平方向的分层,每层扮演应用程序中特定的角色。根据需求和软件复杂度,我们可以设计N层,但大多数应用程序使用3-4层。有太多层的设计会很糟糕,将导致复杂度的上升,因为我们必须维护每一层。在传统的分层架构中,分层包括 表现层、业务或者服务层,以及数据访问层 。 表现层负责应用程序的用户交互和用户体验(外观和视觉)。通常我们会使用 数据传输对象(Data Transfer Object) 将数据带到这一层,然后使用 视图模型(View Model) 渲染到客户端。业务层接收请求并执行业务规则。数据访问层负责操作各种类型的数据库,每个访问数据库的请求都要经过这一层。
分层无需知道其他层如何去做,比如业务层无需知道数据访问层是如何查询数据库的,相反,业务层在调用数据层的特定方法时,只需关注需要部分数据还是全部数据。这就是我们所说的 关注点分离 。这是非常强大的功能,每层负责其所负的责任。
分层架构中的核心概念是管理依赖。如果我们使用依赖倒置原则和测试驱动开发(Test Driven Development),我们的架构会有更好的健壮性。因为,我们要保证所有可能的用例都有测试用例。
我们需要这样的冗余,即使业务层没有处理业务规则,也要通过业务层来调用数据层,这叫 分层隔离 。对于某些功能,如果我们从表现层直接访问数据层,那么数据层后续的任何变动都将影响到业务层和表现层。
分层架构中的一个重要的概念就是分层的开闭原则。如果某层是关闭的,那么每个请求都要经过着一层。相反,如果该层是开放的,那么请求可以绕过这一层,直接到下一层。
分层隔离有利于降低整个应用程序的复杂度。某些功能并不需要经过每一层,这时我们需要根据开闭原则来简化实现。
分层架构是SOLID原则的通用架构,当我们不确定哪种架构更合适的时候,分层架构将是一个很好的起点。我们需要注意防止架构陷入 污水池反模式 。这种反模式描述了请求经过分层,但没做任何事或者只处理了很少的事。如果我们的请求经过所有分层而没有做任何事,这就是 污水池反模式 的征兆。如果20%的请求只是经过各层,而80%的请求实际做事,这还好,如果这个比率不是这样的,那么我们已经患上 反模式综合征 。
此外,分层架构可以演变为 巨石应用(Monolith) ,导致代码库难以维护。

分层架构分析:

2 事件驱动架构
事件驱动架构(Event Driven Architecture)是一种流行的 分布式异步架构 模式,用于创建 可伸缩的应用程序 。这种模式是自适应的,可用于小规模或者大规模的应用程序。事件驱动架构可以与 调停者拓扑(Mediator Topology) 或者 代理者拓扑(Broker Topology) 一起使用。理解拓扑的差异,为应用程序选择正确的拓扑是必不可少的。

调停者拓扑

调停者拓扑需要编排多种事件。比如在交易系统中,每个请求流程必须经过特定的步骤,如验证、订单、配送,以及通知买家等。在这些步骤中,有些可以手动完成,有些可以并行完成。
通常,架构主要包含4种组件,事件队列(Event Queue)、调停者(Mediator)、事件通道(Event Channel)和事件处理器(Event Processor)。客户端创建事件,并将其发送到事件队列,调停者接收事件并将其传递给事件通道。事件通道将事件传递给事件处理器,事件最终由事件处理器处理完成。
事件调停者不会处理也不知道任何业务逻辑,它只编排事件。事件调停者知道每种事件类型的必要步骤。业务逻辑或者处理发生在事件处理器中,事件通道、消息队列或者消息主题用于传递事件给事件处理器。事件处理器是自包含和独立的,解耦于架构。理想情况下,每种事件处理器应只负责处理一种事件类型。
通常,企业服务总线、队列或者集线器可以用作事件调停者。正确选择技术和实现能够降低风险。

代理者拓扑

不像调停者拓扑, 代理者拓扑 不使用任何集中的编排,而是在事件处理器之间使用简单的队列或者集线器,事件处理器知道处理事件的下一个事件处理器。
因其分布式和异步的性质, 事件驱动架构 的实现相对复杂。我们需要面对很多问题,比如网络分区、调停者失败、重新连接逻辑等。由于这是一个分布式且异步的模式,如果你需要事务,那就麻烦了,你得需要一个 事务协调器 。 分布式系统 中的事务非常难以管理,很难找到标准的工作单位模式。
另一个充满挑战的概念是契约。架构师声称服务的契约应该预先定义,而应变是非常昂贵的。

事件驱动架构分析:

3 微内核架构
微内核架构(Microkernel architecture)模式也被称为 插件架构(plugin architecture) 模式。这是产品型应用程序的理想模式,由两部分组成: 核心系统 和插件模块 。核心系统通常包含最小的业务逻辑,并确保能够加载、卸载和运行应用所需的插件。许多操作系统使用这种模式,因此得名微内核。
插件彼此独立,因此解偶。核心系统持有注册器,插件将自己注册其上,因此核心系统知道哪里可以找到它们以及如何运行它们。
这种模式非常适合桌面应用程序,但是也可以在Web应用程序中使用。事实上,许多不同的架构模式可以作为整个系统的一个插件。对于产品型应用程序来说,如果我们想将新特性和功能及时加入系统,微内核架构是一种不错的选择。

微内核架构分析:

4 微服务架构
尽管微服务的概念还相当新,但它确实已经快速地吸引了大量的眼球,以替代整体应用和面向服务架构(SOA)。其中的一个核心概念是具备高可伸缩性、易于部署和交付的独立部署单元(Separately Deployable Units)。最重要的概念是包含业务逻辑和处理流程的服务组件(Service Component)。拿捏粒度设计服务组件是必要而具有挑战性的工作。服务组件是解耦的、分布式的、彼此独立的,并且可以使用已知协议来访问。
微服务的发展是因为整体应用和面向服务应用程序的缺陷。整体应用程序通常包含紧耦合的层,难以部署和交付。比如,如果应用程序总在每次应对变化时垮掉,这是一个因耦合而产生的大问题。微服务将应用程序分解为多个部署单元,因此很容易提升开发和部署能力,以及可测性。虽然面向服务架构非常强大,具有异构连接和松耦合的特性,但是性价比不高。它很复杂、昂贵,难于理解和实现,通常对于大多数应用程序来说矫枉过正。微服务简化了这种复杂性。
跨服务组件的代码冗余是完全正常的。开发微服务时,为了受益于独立的部署单元,以及更加容易的部署,我们可以违反DRY原则。其中的挑战来自服务组件之间的契约,以及服务组件的可用性。

微服务架构分析:




  • UV(Unique visitor):是指通过互联网访问、浏览这个网页的自然人。访问您网站的一台电脑客户端为一个访客。00:00-24:00内相同的客户端只被计算一次。一天内同个访客多次访问仅计算一个UV。
  • IP(Internet Protocol):独立IP是指访问过某站点的IP总数,以用户的IP地址作为统计依据。00:00-24:00内相同IP地址之被计算一次。

  • UV与IP区别:比如你和你的家人用各自的账号在同一台电脑上登录新浪微博,则IP数+1,UV数+2。由于使用的是同一台电脑,所以IP不变,但使用的不同账号,所以UV+2

  • PV(Page View):即页面浏览量或点击量,用户每1次对网站中的每个网页访问均被记录1个PV。用户对同一页面的多次访问,访问量累计,用以衡量网站用户访问的网页数量。

  • VV(Visit View):用以统计所有访客1天内访问网站的次数。当访客完成所有浏览并最终关掉该网站的所有页面时便完成了一次访问,同一访客1天内可能有多次访问行为,访问次数累计。

  • PV与VV区别:比如:你今天10点钟打开了百度,访问了它的三个页面;11点钟又打开了百度,访问了它的两个页面,则PV数+5,VV数+2.PV是指页面的浏览次数,VV是指你访问网站的次数。

  • 高可用:系统 7 x 24 小时不间断服务。
      
  • 高并发,大流量:高并发用户访问,庞大的流量




































个人理解

后面补充



原文
地址:https://www.jianshu.com/p/1eace9e33d8e
大型网站的挑战主要来自庞大的用户,高并发的访问和海量数据,任何简单的业务一旦需要处理数以P计的数据和面对数以亿计的用户,问题就会变得棘手。大型网站架构主要就是解决这类问题。

大型网站系统的特点

高并发,大流量

需要面对高并发用户,大流量访问。Google 日均 PV 35 亿,日 IP 访问数 3 亿;腾讯 QQ 的最大在线用户数 1.4 亿(2011年数据)。

高可用

系统 7 x 24 小时不间断服务。

海量数据

需要存储、管理海量数据,需要使用大量服务器。Facebook 每周上传的照片数量接近 10 亿,百度收录的网页数目有数百亿,Google 有近百万台服务器为全球用户提供服务。
用户分布广泛,网络情况复杂
许多大型互联网站都是为全球用户提供服务的,用户分布范围广,各地网络情况千差万别。在国内,还有各个运营商网络互通难的问题。

安全环境恶劣

由于互联网的开放性,使得互联网站更容易受到攻击,大型网站几乎每天都会被黑客攻击。

需求快速变更,发布频繁

和传统软件的版本发布频率不同,互联网产品为快速适应市场,满足用户需求,其产品发布频率极高。一般大型网站的产品每周都有新版本发布上线,中小型网站的发布更频繁,有时候一天会发布几十次。

渐进式发展

几乎所有的大型互联网网站都是从一个小网站开始,渐进地发展起来的。Facebook 是扎克伯格同学在哈佛大学的宿舍里开发的;Google 的第一台服务器部署在斯坦福大学的实验室;阿里巴巴是在马云家的客厅诞生的。好的互联网产品都是慢慢运营出来的,不是一开始就开发好的,这也正好与网站架构的发展演化过程对应。
大型网站架构演化发展历程
大型网站的技术挑战主要来自于庞大的用户,高并发的访问和海量的数据,任何简单的业务一旦需要处理数以 P 计的数据和面对数以亿计的用户,问题就会变得很棘手。大型网站架构主要解决这类问题。

初始阶段的网站架构

大型网站都是从小型网站发展而来,网站架构也是一样,是从小型网站架构逐步演化而来。小型网站最开始没有太多人访问,只需要一台服务器就绰绰有余,这时的网站架构如下图所示:


应用程序、数据库、文件等所有资源都在一台服务器上。

应用服务和数据服务分离

随着网站业务的发展,一台服务器逐渐不能满足需求:越来越多的用户访问导致性能越来越差,越来越多的数据导致存储空间不足。这时就需要将应用和数据分离。应用和数据分离后整个网站使用3台服务器:应用服务器、文件服务器和数据库服务器。这 3 台服务器对硬件资源的要求各不相同:
应用服务器需要处理大量的业务逻辑,因此需要更快更强大的CPU;
数据库服务器需要快速磁盘检索和数据缓存,因此需要更快的磁盘和更大的内存;
文件服务器需要存储大量用户上传的文件,因此需要更大的硬盘。
此时,网站系统的架构如下图所示:
应用和数据分离后,不同特性的服务器承担不同的服务角色,网站的并发处理能力和数据存储空间得到了很大改善,支持网站业务进一步发展。但是随着用户逐渐增多,网站又一次面临挑战:数据库压力太大导致访问延迟,进而影响整个网站的性能,用户体验受到影响。这时需要对网站架构进一步优化。

使用缓存改善网站性能

网站访问的特点和现实世界的财富分配一样遵循二八定律:80% 的业务访问集中在20% 的数据上。既然大部分业务访问集中在一小部分数据上,那么如果把这一小部分数据缓存在内存中,就可以减少数据库的访问压力,提高整个网站的数据访问速度,改善数据库的写入性能了。 网站使用的缓存可以分为两种:缓存在应用服务器上的本地缓存和缓存在专门的分布式缓存服务器上的远程缓存。
本地缓存的访问速度更快一些,但是受应用服务器内存限制,其缓存数据量有限,而且会出现和应用程序争用内存的情况。
远程分布式缓存可以使用集群的方式,部署大内存的服务器作为专门的缓存服务器,可以在理论上做到不受内存容量限制的缓存服务。
使用缓存后,数据访问压力得到有效缓解,但是单一应用服务器能够处理的请求连接有限,在网站访问高峰期,应用服务器成为整个网站的瓶颈。

使用应用服务器集群改善网站的并发处理能力

使用集群是网站解决高并发、海量数据问题的常用手段。当一台服务器的处理能力、存储空间不足时,不要企图去更换更强大的服务器,对大型网站而言,不管多么强大的服务器,都满足不了网站持续增长的业务需求。这种情况下,更恰当的做法是增加一台服务器分担原有服务器的访问及存储压力。 对网站架构而言,只要能通过增加一台服务器的方式改善负载压力,就可以以同样的方式持续增加服务器不断改善系统性能,从而实现系统的可伸缩性。应用服务器实现集群是网站可伸缩架构设计中较为简单成熟的一种,如下图所示:
通过负载均衡调度服务器,可以将来自用户浏览器的访问请求分发到应用服务器集群中的任何一台服务器上,如果有更多用户,就在集群中加入更多的应用服务器,使应用服务器的压力不再成为整个网站的瓶颈。

数据库读写分离

网站在使用缓存后,使对大部分数据读操作访问都可以不通过数据库就能完成,但是仍有一部分读操作(缓存访问不命中、缓存过期)和全部的写操作都需要访问数据库,在网站的用户达到一定规模后,数据库因为负载压力过高而成为网站的瓶颈。 目前大部分的主流数据库都提供主从热备功能,通过配置两台数据库主从关系,可以将一台数据库服务器的数据更新同步到另一台服务器上。网站利用数据库的这一功能,实现数据库读写分离,从而改善数据库负载压力。如下图所示:
应用服务器在写数据的时候,访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库,这样当应用服务器读数据的时候,就可以通过从数据库获得数据。为了便于应用程序访问读写分离后的数据库,通常在应用服务器端使用专门的数据访问模块,使数据库读写分离对应用透明。

使用反向代理和 CDN 加速网站响应

随着网站业务不断发展,用户规模越来越大,由于中国复杂的网络环境,不同地区的用户访问网站时,速度差别也极大。有研究表明,网站访问延迟和用户流失率正相关,网站访问越慢,用户越容易失去耐心而离开。为了提供更好的用户体验,留住用户,网站需要加速网站访问速度。主要手段有使用 CDN 和反向代理。如下图所示:
CDN 和反向代理的基本原理都是缓存。
CDN 部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据
反向代理则部署在网站的中心机房,当用户请求到达中心机房后,首先访问的服务器是反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户
使用 CDN 和反向代理的目的都是尽早返回数据给用户,一方面加快用户访问速度,另一方面也减轻后端服务器的负载压力。

使用分布式文件系统和分布式数据库系统

任何强大的单一服务器都满足不了大型网站持续增长的业务需求。数据库经过读写分离后,从一台服务器拆分成两台服务器,但是随着网站业务的发展依然不能满足需求,这时需要使用分布式数据库。文件系统也一样,需要使用分布式文件系统。如下图所示:
分布式数据库是网站数据库拆分的最后手段,只有在单表数据规模非常庞大的时候才使用。不到不得已时,网站更常用的数据库拆分手段是业务分库,将不同业务的数据部署在不同的物理服务器上。

使用 NoSQL 和搜索引擎

随着网站业务越来越复杂,对数据存储和检索的需求也越来越复杂,网站需要采用一些非关系数据库技术如 NoSQL 和非数据库查询技术如搜索引擎。如下图所示:
NoSQL 和搜索引擎都是源自互联网的技术手段,对可伸缩的分布式特性具有更好的支持。应用服务器则通过一个统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。

业务拆分

大型网站为了应对日益复杂的业务场景,通过使用分而治之的手段将整个网站业务分成不同的产品线。如大型购物交易网站都会将首页、商铺、订单、买家、卖家等拆分成不同的产品线,分归不同的业务团队负责。
具体到技术上,也会根据产品线划分,将一个网站拆分成许多不同的应用,每个应用独立部署。应用之间可以通过一个超链接建立关系(在首页上的导航链接每个都指向不同的应用地址),也可以通过消息队列进行数据分发,当然最多的还是通过访问同一个数据存储系统来构成一个关联的完整系统,如下图所示:

分布式服务

随着业务拆分越来越小,存储系统越来越庞大,应用系统的整体复杂度呈指数级增加,部署维护越来越困难。由于所有应用要和所有数据库系统连接,在数万台服务器规模的网站中,这些连接的数目是服务器规模的平方,导致数据库连接资源不足,拒绝服务。
既然每一个应用系统都需要执行许多相同的业务操作,比如用户管理、商品管理等,那么可以将这些共用的业务提取出来,独立部署。由这些可复用的业务连接数据库,提供共用业务服务,而应用系统只需要管理用户界面,通过分布式服务调用共用业务服务完成具体业务操作。如下图所示:
大型网站的架构演化到这里,基本上大多数的技术问题都得以解决,诸如跨数据中心的实时数据同步和具体网站业务相关的问题也都可以通过组合改进现有技术架构解决。


作者:java高级分享
链接:https://www.jianshu.com/p/1eace9e33d8e
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


重拾后端之Spring Boot(四):使用JWT和Spring Security保护REST API

 
接灰的电子产品 关注
2017.03.10 20:39* 字数 4519 阅读 50882评论 144喜欢 262赞赏 2
重拾后端之Spring Boot(一):REST API的搭建可以这样简单
重拾后端之Spring Boot(二):MongoDb的无缝集成
重拾后端之Spring Boot(三):找回熟悉的Controller,Service
重拾后端之Spring Boot(四):使用 JWT 和 Spring Security 保护 REST API
重拾后端之Spring Boot(五):跨域、自定义查询及分页
重拾后端之Spring Boot(六):热加载、容器和多项目
通常情况下,把API直接暴露出去是风险很大的,不说别的,直接被机器攻击就喝一壶的。那么一般来说,对API要划分出一定的权限级别,然后做一个用户的鉴权,依据鉴权结果给予用户开放对应的API。目前,比较主流的方案有几种:
  1. 用户名和密码鉴权,使用Session保存用户鉴权结果。
  2. 使用OAuth进行鉴权(其实OAuth也是一种基于Token的鉴权,只是没有规定Token的生成方式)
  3. 自行采用Token进行鉴权
第一种就不介绍了,由于依赖Session来维护状态,也不太适合移动时代,新的项目就不要采用了。第二种OAuth的方案和JWT都是基于Token的,但OAuth其实对于不做开放平台的公司有些过于复杂。我们主要介绍第三种:JWT。

什么是JWT?

JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧  自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。

JWT的工作流程

下面是一个JWT的工作流程图。模拟一下实际的流程是这样的(假设受保护的API在/protected中)
  1. 用户导航到登录页,输入用户名、密码,进行登录
  2. 服务器验证登录鉴权,如果改用户合法,根据用户的信息和服务器的规则生成JWT Token
  3. 服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
  4. 用户得到token,存在localStorage、cookie或其它数据存储形式中。
  5. 以后用户请求/protected中的API时,在请求的header中加入 Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer
  6. 服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
  7. 用户取得结果
JWT工作流程图
为了更好的理解这个token是什么,我们先来看一个token生成后的样子,下面那坨乱糟糟的就是了。
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ.RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg
但仔细看到的话还是可以看到这个token分成了三部分,每部分用 . 分隔,每段都是用 Base64 编码的。如果我们用一个Base64的解码器的话 ( https://www.base64decode.org/ ),可以看到第一部分 eyJhbGciOiJIUzUxMiJ9 被解析成了:
{
"alg":"HS512"
}

这是告诉我们HMAC采用HS512算法对JWT进行的签名。
第二部分 eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ 被解码之后是
{
"sub":"wang",
"created":1489079981393,
"exp":1489684781
}

这段告诉我们这个Token中含有的数据声明(Claim),这个例子里面有三个声明:sub, created  exp。在我们这个例子中,分别代表着用户名、创建时间和过期时间,当然你可以把任意数据声明在这里。
看到这里,你可能会想这是个什么鬼token,所有信息都透明啊,安全怎么保障?别急,我们看看token的第三段 RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg。同样使用Base64解码之后,咦,这是什么东东
D X �DmYTeȧL�UZcPZ0$gZAY�_7�wY@
最后一段其实是签名,这个签名必须知道秘钥才能计算。这个也是JWT的安全保障。这里提一点注意事项,由于数据声明(Claim)是公开的,千万不要把密码等敏感字段放进去,否则就等于是公开给别人了。
也就是说JWT是由三段组成的,按官方的叫法分别是header(头)、payload(负载)和signature(签名):
header.payload.signature
头中的数据通常包含两部分:一个是我们刚刚看到的 alg,这个词是 algorithm 的缩写,就是指明算法。另一个可以添加的字段是token的类型(按RFC 7519实现的token机制不只JWT一种),但如果我们采用的是JWT的话,指定这个就多余了。
{
"alg": "HS512",
"typ": "JWT"
}
payload中可以放置三类数据:系统保留的、公共的和私有的:
  • 系统保留的声明(Reserved claims):这类声明不是必须的,但是是建议使用的,包括:iss (签发者), exp (过期时间),
    sub (主题), aud (目标受众)等。这里我们发现都用的缩写的三个字符,这是由于JWT的目标就是尽可能小巧。
  • 公共声明:这类声明需要在 IANA JSON Web Token Registry 中定义或者提供一个URI,因为要避免重名等冲突。
  • 私有声明:这个就是你根据业务需要自己定义的数据了。
签名的过程是这样的:采用header中声明的算法,接受三个参数:base64编码的header、base64编码的payload和秘钥(secret)进行运算。签名这一部分如果你愿意的话,可以采用RSASHA256的方式进行公钥、私钥对的方式进行,如果安全性要求的高的话。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

JWT的生成和解析

为了简化我们的工作,这里引入一个比较成熟的JWT类库,叫 jjwt ( https://github.com/jwtk/jjwt )。这个类库可以用于Java和Android的JWT token的生成和验证。
JWT的生成可以使用下面这样的代码完成:
String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret) //采用什么算法是可以自己选择的,不一定非要采用HS512
.compact();
}
数据声明(Claim)其实就是一个Map,比如我们想放入用户名,可以简单的创建一个Map然后put进去就可以了。
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, username());
解析也很简单,利用 jjwt 提供的parser传入秘钥,然后就可以解析token了。
Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
JWT本身没啥难度,但安全整体是一个比较复杂的事情,JWT只不过提供了一种基于token的请求验证机制。但我们的用户权限,对于API的权限划分、资源的权限划分,用户的验证等等都不是JWT负责的。也就是说,请求验证后,你是否有权限看对应的内容是由你的用户角色决定的。所以我们这里要利用Spring的一个子项目Spring Security来简化我们的工作。

Spring Security

Spring Security是一个基于Spring的通用安全框架,里面内容太多了,本文的主要目的也不是展开讲这个框架,而是如何利用Spring Security和JWT一起来完成API保护。所以关于Spring Secruity的基础内容或展开内容,请自行去官网学习( http://projects.spring.io/spring-security/ )。

简单的背景知识

如果你的系统有用户的概念的话,一般来说,你应该有一个用户表,最简单的用户表,应该有三列:Id,Username和Password,类似下表这种
ID
USERNAME
PASSWORD
10
wang
abcdefg
而且不是所有用户都是一种角色,比如网站管理员、供应商、财务等等,这些角色和网站的直接用户需要的权限可能是不一样的。那么我们就需要一个角色表:
ID
ROLE
10
USER
20
ADMIN
当然我们还需要一个可以将用户和角色关联起来建立映射关系的表。
USER_ID
ROLE_ID
10
10
20
20
这是典型的一个关系型数据库的用户角色的设计,由于我们要使用的MongoDB是一个文档型数据库,所以让我们重新审视一下这个结构。
这个数据结构的优点在于它避免了数据的冗余,每个表负责自己的数据,通过关联表进行关系的描述,同时也保证的数据的完整性:比如当你修改角色名称后,没有脏数据的产生。
但是这种事情在用户权限这个领域发生的频率到底有多少呢?有多少人每天不停的改的角色名称?当然如果你的业务场景确实是需要保证数据完整性,你还是应该使用关系型数据库。但如果没有高频的对于角色表的改动,其实我们是不需要这样的一个设计的。在MongoDB中我们可以将其简化为
{
_id: <id_generated>
username: 'user',
password: 'pass',
roles: ['USER', 'ADMIN']
}
基于以上考虑,我们重构一下 User 类,
@Datapublic class User {
@Id
private String id;

@Indexed(unique=true, direction= IndexDirection.DESCENDING, dropDups=true)
private String username;

private String password;
private String email;
private Date lastPasswordResetDate;
private List<String> roles;
}
当然你可能发现这个类有点怪,只有一些field,这个简化的能力是一个叫lombok类库提供的 ,这个很多开发过Android的童鞋应该熟悉,是用来简化POJO的创建的一个类库。简单说一下,采用 lombok 提供的 @Data 修饰符后可以简写成,原来的一坨getter和setter以及constructor等都不需要写了。类似的 Todo 可以改写成:
@Datapublic class Todo {
@Id private String id;
private String desc;
private boolean completed;
private User user;
}
增加这个类库只需在 build.gradle 中增加下面这行
dependencies {
// 省略其它依赖
compile("org.projectlombok:lombok:${lombokVersion}")
}

引入Spring Security

要在Spring Boot中引入Spring Security非常简单,修改 build.gradle,增加一个引用 org.springframework.boot:spring-boot-starter-security
dependencies {
compile("org.springframework.boot:spring-boot-starter-data-rest")
compile("org.springframework.boot:spring-boot-starter-data-mongodb")
compile("org.springframework.boot:spring-boot-starter-security")
compile("io.jsonwebtoken:jjwt:${jjwtVersion}")
compile("org.projectlombok:lombok:${lombokVersion}")
testCompile("org.springframework.boot:spring-boot-starter-test")
}
你可能发现了,我们不只增加了对Spring Security的编译依赖,还增加 jjwt 的依赖。
Spring Security需要我们实现几个东西,第一个是UserDetails:这个接口中规定了用户的几个必须要有的方法,所以我们创建一个JwtUser类来实现这个接口。为什么不直接使用User类?因为这个UserDetails完全是为了安全服务的,它和我们的领域类可能有部分属性重叠,但很多的接口其实是安全定制的,所以最好新建一个类:
public class JwtUser implements UserDetails {
private final String id;
private final String username;
private final String password;
private final String email;
private final Collection<? extends GrantedAuthority> authorities;
private final Date lastPasswordResetDate;

public JwtUser(
String id,
String username,
String password,
String email,
Collection<? extends GrantedAuthority> authorities,
Date lastPasswordResetDate) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.authorities = authorities;
this.lastPasswordResetDate = lastPasswordResetDate;
}
//返回分配给用户的角色列表
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
public String getId() {
return id;
}

@JsonIgnore
@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}
// 账户是否未过期
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
// 账户是否未锁定
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
// 密码是否未过期
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 账户是否激活
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
// 这个是自定义的,返回上次密码重置日期
@JsonIgnore
public Date getLastPasswordResetDate() {
return lastPasswordResetDate;
}
}
这个接口中规定的很多方法我们都简单粗暴的设成直接返回某个值了,这是为了简单起见,你在实际开发环境中还是要根据具体业务调整。当然由于两个类还是有一定关系的,为了写起来简单,我们写一个工厂类来由领域对象创建 JwtUser,这个工厂就叫 JwtUserFactory 吧:
public final class JwtUserFactory {

private JwtUserFactory() {
}

public static JwtUser create(User user) {
return new JwtUser(
user.getId(),
user.getUsername(),
user.getPassword(),
user.getEmail(),
mapToGrantedAuthorities(user.getRoles()),
user.getLastPasswordResetDate()
);
}

private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
第二个要实现的是 UserDetailsService,这个接口只定义了一个方法 loadUserByUsername,顾名思义,就是提供一种从用户名可以查到用户并返回的方法。注意,不一定是数据库哦,文本文件、xml文件等等都可能成为数据源,这也是为什么Spring提供这样一个接口的原因:保证你可以采用灵活的数据源。接下来我们建立一个 JwtUserDetailsServiceImpl来实现这个接口。
@Servicepublic class JwtUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);

if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
} else {
return JwtUserFactory.create(user);
}
}
}

为了让Spring可以知道我们想怎样控制安全性,我们还需要建立一个安全配置类 WebSecurityConfig
@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

// Spring会自动寻找同样类型的具体类注入,这里就是JwtUserDetailsServiceImpl了
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
// 设置UserDetailsService
.userDetailsService(this.userDetailsService)
// 使用BCrypt进行密码的hash
.passwordEncoder(passwordEncoder());
}
// 装载BCrypt密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 由于使用的是JWT,我们这里不需要csrf
.csrf().disable()

// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

.authorizeRequests()
//.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

// 允许对于网站静态资源的无授权访问
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
// 对于获取token的rest api要允许匿名访问
.antMatchers("/auth/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();

// 禁用缓存
httpSecurity.headers().cacheControl();
}
}
接下来我们要规定一下哪些资源需要什么样的角色可以访问了,在 UserController 加一个修饰符 @PreAuthorize("hasRole('ADMIN')") 表示这个资源只能被拥有 ADMIN 角色的用户访问。
/**
* 在 @PreAuthorize 中我们可以利用内建的 SPEL 表达式:比如 'hasRole()' 来决定哪些用户有权访问。
* 需注意的一点是 hasRole 表达式认为每个角色名字前都有一个前缀 'ROLE_'。所以这里的 'ADMIN' 其实在
* 数据库中存储的是 'ROLE_ADMIN' 。这个 @PreAuthorize 可以修饰Controller也可修饰Controller中的方法。
**/@RestController@RequestMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public class UserController {
@Autowired
private UserRepository repository;

@RequestMapping(method = RequestMethod.GET)
public List<User> getUsers() {
return repository.findAll();
}

// 略去其它部分
}
类似的我们给 TodoController 加上 @PreAuthorize("hasRole('USER')"),标明这个资源只能被拥有 USER 角色的用户访问:
@RestController@RequestMapping("/todos")
@PreAuthorize("hasRole('USER')")
public class TodoController {
// 略去
}

使用application.yml配置SpringBoot应用

现在应该Spring Security可以工作了,但为了可以更清晰的看到工作日志,我们希望配置一下,在和 src 同级建立一个config文件夹,在这个文件夹下面新建一个 application.yml
# Server configuration
server:
port: 8090
contextPath:

# Spring configuration
spring:
jackson:
serialization:
INDENT_OUTPUT: true
data.mongodb:
host: localhost
port: 27017
database: springboot

# Logging configuration
logging:
level:
org.springframework:
data: DEBUG
security: DEBUG

我们除了配置了logging的一些东东外,也顺手设置了数据库和http服务的一些配置项,现在我们的服务器会在8090端口监听,而spring data和security的日志在debug模式下会输出到console。
现在启动服务后,访问 http://localhost:8090 你可以看到根目录还是正常显示的
根目录还是正常可以访问的
但我们试一下 http://localhost:8090/users ,观察一下console,我们会看到如下的输出,告诉由于用户未鉴权,我们访问被拒绝了。
2017-03-10 15:51:53.351 DEBUG 57599 --- [nio-8090-exec-4] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point

org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.2.1.RELEASE.jar:4.2.1.RELEASE]

集成JWT和Spring Security

到现在,我们还是让JWT和Spring Security各自为战,并没有集成起来。要想要JWT在Spring中工作,我们应该新建一个filter,并把它配置在 WebSecurityConfig 中。
@Componentpublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Value("${jwt.header}")
private String tokenHeader;

@Value("${jwt.tokenHead}")
private String tokenHead;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
if (authHeader != null && authHeader.startsWith(tokenHead)) {
final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer "
String username = jwtTokenUtil.getUsernameFromToken(authToken);

logger.info("checking authentication " + username);

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
logger.info("authenticated user " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}

chain.doFilter(request, response);
}
}
事实上如果我们足够相信token中的数据,也就是我们足够相信签名token的secret的机制足够好,这种情况下,我们可以不用再查询数据库,而直接采用token中的数据。本例中,我们还是通过Spring Security的 @UserDetailsService 进行了数据查询,但简单验证的话,你可以采用直接验证token是否合法来避免昂贵的数据查询。
接下来,我们会在 WebSecurityConfig 中注入这个filter,并且配置到 HttpSecurity 中:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

// 省略其它部分

@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 省略之前写的规则部分,具体看前面的代码

// 添加JWT filter
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
}

完成鉴权(登录)、注册和更新token的功能

到现在,我们整个API其实已经在安全的保护下了,但我们遇到一个问题:所有的API都安全了,但我们还没有用户啊,所以所有API都没法访问。因此要提供一个注册、登录的API,这个API应该是可以匿名访问的。给它规划的路径呢,我们前面其实在WebSecurityConfig中已经给出了,就是 /auth
首先需要一个AuthService,规定一下必选动作:
public interface AuthService {
User register(User userToAdd);
String login(String username, String password);
String refresh(String oldToken);
}
然后,实现这些必选动作,其实非常简单:
  1. 登录时要生成token,完成Spring Security认证,然后返回token给客户端
  2. 注册时将用户密码用BCrypt加密,写入用户角色,由于是开放注册,所以写入角色系统控制,将其写成 ROLE_USER
  3. 提供一个可以刷新token的接口 refresh 用于取得新的token
@Servicepublic class AuthServiceImpl implements AuthService {

private AuthenticationManager authenticationManager;
private UserDetailsService userDetailsService;
private JwtTokenUtil jwtTokenUtil;
private UserRepository userRepository;

@Value("${jwt.tokenHead}")
private String tokenHead;

@Autowired
public AuthServiceImpl(
AuthenticationManager authenticationManager,
UserDetailsService userDetailsService,
JwtTokenUtil jwtTokenUtil,
UserRepository userRepository) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
this.userRepository = userRepository;
}

@Override
public User register(User userToAdd) {
final String username = userToAdd.getUsername();
if(userRepository.findByUsername(username)!=null) {
return null;
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
final String rawPassword = userToAdd.getPassword();
userToAdd.setPassword(encoder.encode(rawPassword));
userToAdd.setLastPasswordResetDate(new Date());
userToAdd.setRoles(asList("ROLE_USER"));
return userRepository.insert(userToAdd);
}

@Override
public String login(String username, String password) {
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);

final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
final String token = jwtTokenUtil.generateToken(userDetails);
return token;
}

@Override
public String refresh(String oldToken) {
final String token = oldToken.substring(tokenHead.length());
String username = jwtTokenUtil.getUsernameFromToken(token);
JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())){
return jwtTokenUtil.refreshToken(token);
}
return null;
}
}
然后建立AuthController就好,这个AuthController中我们在其中使用了表达式绑定,比如 @Value("${jwt.header}")中的 jwt.header 其实是定义在 applicaiton.yml 中的
# JWT
jwt:
header: Authorization
secret: mySecret
expiration: 604800
tokenHead: "Bearer "
route:
authentication:
path: auth
refresh: refresh
register: "auth/register"
同样的 @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST) 中的 jwt.route.authentication.path 也是定义在上面的
@RestControllerpublic class AuthController {
@Value("${jwt.header}")
private String tokenHeader;

@Autowired
private AuthService authService;

@RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(
@RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException{
final String token = authService.login(authenticationRequest.getUsername(), authenticationRequest.getPassword());

// Return the token
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}

@RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET)
public ResponseEntity<?> refreshAndGetAuthenticationToken(
HttpServletRequest request) throws AuthenticationException{
String token = request.getHeader(tokenHeader);
String refreshedToken = authService.refresh(token);
if(refreshedToken == null) {
return ResponseEntity.badRequest().body(null);
} else {
return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));
}
}

@RequestMapping(value = "${jwt.route.authentication.register}", method = RequestMethod.POST)
public User register(@RequestBody User addedUser) throws AuthenticationException{
return authService.register(addedUser);
}
}

验证时间

接下来,我们就可以看看我们的成果了,首先注册一个用户 peng2,很完美的注册成功了
注册用户
然后在 /auth 中取得token,也很成功
取得token
不使用token时,访问 /users 的结果,不出意料的失败,提示未授权。
不使用token访问users列表
使用token时,访问 /users 的结果,虽然仍是失败,但这次提示访问被拒绝,意思就是虽然你已经得到了授权,但由于你的会员级别还只是普卡会员,所以你的请求被拒绝。
image_1bas22va52vk1rj445fhm87k72a.png-156.9kB
接下来我们访问 /users/?username=peng2,竟然可以访问啊
访问自己的信息是允许的
这是由于我们为这个方法定义的权限就是:拥有ADMIN角色或者是当前用户本身。Spring Security真是很方便,很强大。
@PostAuthorize("returnObject.username == principal.username or hasRole('ROLE_ADMIN')")
@RequestMapping(value = "/",method = RequestMethod.GET)
public User getUserByUsername(@RequestParam(value="username") String username) {
return repository.findByUsername(username);
}
本章代码: https://github.com/wpcfan/spring-boot-tut/tree/chap04
慕课网 Angular 视频课上线: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner
重拾后端之Spring Boot(一):REST API的搭建可以这样简单
重拾后端之Spring Boot(二):MongoDb的无缝集成
重拾后端之Spring Boot(三):找回熟悉的Controller,Service
重拾后端之Spring Boot(四):使用 JWT 和 Spring Security 保护 REST API
重拾后端之Spring Boot(五):跨域、自定义查询及分页
有问题的童鞋可以加入我的小密圈讨论: http://t.xiaomiquan.com/jayRnaQ (该链接7天内(5月14日前)有效)

个人理解
1.对作者的这句话有疑问,
竞争机制支持公平和非公平两种:非公平竞争模式使用的数据结构是后进先出栈(Lifo Stack);公平竞争模式则使用先进先出队列(Fifo Queue),性能上两者是相当的,一般情况下,Fifo通常可以支持更大的吞吐量,但Lifo可以更大程度的保持线程的本地化
 其实非公平竞争锁具有更高的性能和吞吐量,这儿是对同一个概念的误解,还是本初特殊的公平和非公平?
原文
地址:http://ifeve.com/java-synchronousqueue/

介绍

Java 6的并发编程包中的SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。
不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。
SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

实现原理

阻塞队列的实现方法有许多:

阻塞算法实现

阻塞算法实现通常在内部采用一个锁来保证多个线程中的put()和take()方法是串行执行的。采用锁的开销是比较大的,还会存在一种情况是线程A持有线程B需要的锁,B必须一直等待A释放锁,即使A可能一段时间内因为B的优先级比较高而得不到时间片运行。所以在高性能的应用中我们常常希望规避锁的使用。
01
public class NativeSynchronousQueue<E> {
02
    boolean putting = false;
03
    E item = null;
04
 
05
    public synchronized E take() throws InterruptedException {
06
        while (item == null)
07
            wait();
08
        E e = item;
09
        item = null;
10
        notifyAll();
11
        return e;
12
    }
13
 
14
    public synchronized void put(E e) throws InterruptedException {
15
        if (e==null) return;
16
        while (putting)
17
            wait();
18
        putting = true;
19
        item = e;
20
        notifyAll();
21
        while (item!=null)
22
            wait();
23
        putting = false;
24
        notifyAll();
25
    }
26
}

信号量实现

经典同步队列实现采用了三个信号量,代码很简单,比较容易理解:
01
public class SemaphoreSynchronousQueue<E> {
02
    E item = null;
03
    Semaphore sync = new Semaphore(0);
04
    Semaphore send = new Semaphore(1);
05
    Semaphore recv = new Semaphore(0);
06
 
07
    public E take() throws InterruptedException {
08
        recv.acquire();
09
        E x = item;
10
        sync.release();
11
        send.release();
12
        return x;
13
    }
14
 
15
    public void put (E x) throws InterruptedException{
16
        send.acquire();
17
        item = x;
18
        recv.release();
19
        sync.acquire();
20
    }
21
}
在多核机器上,上面方法的同步代价仍然较高,操作系统调度器需要上千个时间片来阻塞或唤醒线程,而上面的实现即使在生产者put()时已经有一个消费者在等待的情况下,阻塞和唤醒的调用仍然需要。

Java 5实现

01
public class Java5SynchronousQueue<E> {
02
    ReentrantLock qlock = new ReentrantLock();
03
    Queue waitingProducers = new Queue();
04
    Queue waitingConsumers = new Queue();
05
 
06
    static class Node extends AbstractQueuedSynchronizer {
07
        E item;
08
        Node next;
09
 
10
        Node(Object x) { item = x; }
11
        void waitForTake() { /* (uses AQS) */ }
12
           E waitForPut() { /* (uses AQS) */ }
13
    }
14
 
15
    public E take() {
16
        Node node;
17
        boolean mustWait;
18
        qlock.lock();
19
        node = waitingProducers.pop();
20
        if(mustWait = (node == null))
21
           node = waitingConsumers.push(null);
22
         qlock.unlock();
23
 
24
        if (mustWait)
25
           return node.waitForPut();
26
        else
27
            return node.item;
28
    }
29
 
30
    public void put(E e) {
31
         Node node;
32
         boolean mustWait;
33
         qlock.lock();
34
         node = waitingConsumers.pop();
35
         if (mustWait = (node == null))
36
             node = waitingProducers.push(e);
37
         qlock.unlock();
38
 
39
         if (mustWait)
40
             node.waitForTake();
41
         else
42
            node.item = e;
43
    }
44
}
Java 5的实现相对来说做了一些优化,只使用了一个锁,使用队列代替信号量也可以允许发布者直接发布数据,而不是要首先从阻塞在信号量处被唤醒。

Java6实现

Java 6的SynchronousQueue的实现采用了一种性能更好的无锁算法 — 扩展的“Dual stack and Dual queue”算法。性能比Java5的实现有较大提升。竞争机制支持公平和非公平两种:非公平竞争模式使用的数据结构是后进先出栈(Lifo Stack);公平竞争模式则使用先进先出队列(Fifo Queue),性能上两者是相当的,一般情况下,Fifo通常可以支持更大的吞吐量,但Lifo可以更大程度的保持线程的本地化。
代码实现里的Dual Queue或Stack内部是用链表(LinkedList)来实现的,其节点状态为以下三种情况:
  1. 持有数据 – put()方法的元素
  2. 持有请求 – take()方法
这个算法的特点就是任何操作都可以根据节点的状态判断执行,而不需要用到锁。
其核心接口是Transfer,生产者的put或消费者的take都使用这个接口,根据第一个参数来区别是入列(栈)还是出列(栈)。
01
/**
02
    * Shared internal API for dual stacks and queues.
03
    */
04
   static abstract class Transferer {
05
       /**
06
        * Performs a put or take.
07
        *
08
        * @param e if non-null, the item to be handed to a consumer;
09
        *          if null, requests that transfer return an item
10
        *          offered by producer.
11
        * @param timed if this operation should timeout
12
        * @param nanos the timeout, in nanoseconds
13
        * @return if non-null, the item provided or received; if null,
14
        *         the operation failed due to timeout or interrupt --
15
        *         the caller can distinguish which of these occurred
16
        *         by checking Thread.interrupted.
17
        */
18
       abstract Object transfer(Object e, boolean timed, long nanos);
19
   }
TransferQueue实现如下(摘自Java 6源代码),入列和出列都基于Spin和CAS方法:
01
/**
02
    * Puts or takes an item.
03
    */
04
   Object transfer(Object e, boolean timed, long nanos) {
05
       /* Basic algorithm is to loop trying to take either of
06
        * two actions:
07
        *
08
        * 1. If queue apparently empty or holding same-mode nodes,
09
        *    try to add node to queue of waiters, wait to be
10
        *    fulfilled (or cancelled) and return matching item.
11
        *
12
        * 2. If queue apparently contains waiting items, and this
13
        *    call is of complementary mode, try to fulfill by CAS'ing
14
        *    item field of waiting node and dequeuing it, and then
15
        *    returning matching item.
16
        *
17
        * In each case, along the way, check for and try to help
18
        * advance head and tail on behalf of other stalled/slow
19
        * threads.
20
        *
21
        * The loop starts off with a null check guarding against
22
        * seeing uninitialized head or tail values. This never
23
        * happens in current SynchronousQueue, but could if
24
        * callers held non-volatile/final ref to the
25
        * transferer. The check is here anyway because it places
26
        * null checks at top of loop, which is usually faster
27
        * than having them implicitly interspersed.
28
        */
29
 
30
       QNode s = null; // constructed/reused as needed
31
       boolean isData = (e != null);
32
 
33
       for (;;) {
34
           QNode t = tail;
35
           QNode h = head;
36
           if (t == null || h == null)         // saw uninitialized value
37
               continue;                       // spin
38
 
39
           if (h == t || t.isData == isData) { // empty or same-mode
40
               QNode tn = t.next;
41
               if (t != tail)                  // inconsistent read
42
                   continue;
43
               if (tn != null) {               // lagging tail
44
                   advanceTail(t, tn);
45
                   continue;
46
               }
47
               if (timed &amp;&amp; nanos &lt;= 0)        // can't wait
48
                   return null;
49
               if (s == null)
50
                   s = new QNode(e, isData);
51
               if (!t.casNext(null, s))        // failed to link in
52
                   continue;
53
 
54
               advanceTail(t, s);              // swing tail and wait
55
               Object x = awaitFulfill(s, e, timed, nanos);
56
               if (x == s) {                   // wait was cancelled
57
                   clean(t, s);
58
                   return null;
59
               }
60
 
61
               if (!s.isOffList()) {           // not already unlinked
62
                   advanceHead(t, s);          // unlink if head
63
                   if (x != null)              // and forget fields
64
                       s.item = s;
65
                   s.waiter = null;
66
               }
67
               return (x != null)? x : e;
68
 
69
           } else {                            // complementary-mode
70
               QNode m = h.next;               // node to fulfill
71
               if (t != tail || m == null || h != head)
72
                   continue;                   // inconsistent read
73
 
74
               Object x = m.item;
75
               if (isData == (x != null) ||    // m already fulfilled
76
                   x == m ||                   // m cancelled
77
                   !m.casItem(x, e)) {         // lost CAS
78
                   advanceHead(h, m);          // dequeue and retry
79
                   continue;
80
               }
81
 
82
               advanceHead(h, m);              // successfully fulfilled
83
               LockSupport.unpark(m.waiter);
84
               return (x != null)? x : e;
85
           }
86
       }
87
   }

参考文章

  1. Javadoc of SynchronousQueue
  2. Scalable Synchronous Queues
  3. Nonblocking Concurrent Data Structures with Condition Synchronization
原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: Java并发包中的同步队列SynchronousQueue实现原理


公平锁(Fair)
概念
加锁前检查是否有排队等待的线程,优先排队等待的线程获取锁,先来先得。

哪些是公平锁
juc包中,在构造方法中显式指定公平模式的锁
eg:
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

优点
能够减少饥饿发生的概率,等待越久的线程越容易获取到锁。
缺点
没有非公平锁效率高

非公平锁(Nonfair)
概念
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待

哪些是非公平锁
内部锁(synchronized )和juc包中,在构造方法中显式指定非公平模式的锁

优点
效率更高,在锁竞争剧烈时, 性能比公平锁高5~10倍

缺点
容易发生线程饥饿

为什么非公平锁比公平锁性能更高?
在锁竞争剧烈的情况下,线程如果能够直接获取到锁,就能够直接获取锁,一定程度上可以减少线程调度和上下文切换的开销。
同时公平锁在获取锁的时候,会先检测当前线程是否是等待队列中的第一个节点,如果不是,则无法获取锁,转而切换下个线程尝试获取锁操作。 这样便会带来多次的线程尝试,从而导致上下文切换次数变多,时间也就变长。


一.任务执行框架顶层接口Executor
   源码如下:
   public interface Executor {
     //执行任务,一个任务就是一个Runnable
     void execute(Runnable command);
   }
   Executor提供了一个任务执行的基本接口,从最上层引入了关于任务执行策略的抽象,常常被用来替代显式的自实例化线程执行任务,具有更强的伸缩性、扩展性和可控性。

二.任务执行服务接口ExecutorService
   该接口继承自Executor接口,源码如下:
   public interface ExecutorService extends Executor {
    //关闭任务执行服务,新任务将不会被接受,但已经提交的任务将会被全部执行
    void shutdown();

    //立即关闭任务执行服务,新任务将不会被接受,已经提交的任务将不会执行,正在执行的服务将会被打断。
    //返回已经被提交的,但是还没有执行的任务
    List<Runnable> shutdownNow();

    //判断执行服务是否被关闭
    boolean isShutdown();

    //判断当前所有任务是否都已经在shutdown之后完成,包括被中断的任务,但不包括尚未执行的任务
    boolean isTerminated();

    //这个方法在shutdown之后可以调用
    //目的是阻塞当前线程直到所有任务都已经完成或者时间超时
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
    
    //提交一个带返回值的任务,返回future类型的对象,该对象封装了运行结果、运行异常且支持取消执行等操作
    <T> Future<T> submit(Callable<T> task);

    //提交一个不带返回值的任务,不过会将第二个参数作为该任务的返回值
    <T> Future<T> submit(Runnable task, T result);

    //提交一个不带返回值的任务,不过会返回一个future对象,用于实现任务执行的控制
    Future<?> submit(Runnable task);

    //批量执行带返回值任务,阻塞直到全部任务完成
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;

    //批量执行带返回值任务,阻塞直到全部任务完成或者超时
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    //批量执行带返回值的任务,直到一个任务运行完成则返回该任务的运行结果
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

    // 批量执行带返回值,不过只返回一个成功执行的任务的结果,带超时
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
   }

   Executor接口只提供了一个基本的任务执行接口,ExecutorService继承自Executor,提供了一系列的服务周期控制方法和对任务执行的扩展接口。

三.任务执行接口Runable(不属于并发包)
   源码如下:
   @FunctionalInterface
   public interface Runnable {
      public abstract void run();
   }

四.带返回值和可抛出异常的任务执行接口Callable
   源码如下:
  @FunctionalInterface
  public interface Callable<V> {
    V call() throws Exception;
  }

  Runable接口可以直接给线程使用,Callable接口只能依托FutureTask而运行,因为它带返回值的魔法需要FutureTask的包装

五.封装任务执行结果和任务控制的接口Future
   源码如下:
   public interface Future<V> {

       //尝试取消任务的执行,在以下场景将会失败:
       //1.任务已经完成
       //2.任务已经被取消
       //3.一些其他的原因
       //对于没有开始运行的任务,这些任务后面将不会运行,如果任务已经开始运行,那么mayInterruptIfRunning参数将决定是否打断该线
       //程,从而让该任务自己在合适的时候检测中断。从而自己停止任务
    boolean cancel(boolean mayInterruptIfRunning);

    //判断任务是否已经被取消
    boolean isCancelled();

    //判断任务是否已经完成
    boolean isDone();

    //获取运行结果,
    //如果尚未运行完毕,将会阻塞直到运行完毕或者爆出异常
    //get方法将会获取运行结果或者抛出运行时遇到的异常(被封装在ExecutionException中)
    V get() throws InterruptedException, ExecutionException;

    //获取运行结果,带超时机制
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
   }

六.封装任务执行结果和任务控制的运行任务Runnable接口
   该接口继承自Runable和Future,是任务执行框架当中每个任务执行时的抽象
    源码如下:
    public interface RunnableFuture<V> extends Runnable, Future<V> {
        //再一次申明run方法,表示在运行结束后将会设置运行结果,除非被取消
        void run();
    }

七.任务运行的封装实现FutureTask
   该类实现RunnableFuture,源码如下:
   public class FutureTask<V> implements RunnableFuture<V> {
    //任务运行状态
    private volatile int state;
    //几个状态常量
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;

    //被封装的callable,我们写的逻辑会在这里面
    private Callable<V> callable;

    //返回值或者运行异常
    private Object outcome; // non-volatile, protected by state reads/writes

    //当前任务所运行的线程,通过cas进行保护
    private volatile Thread runner;

    //等待当前线程运行结果的节点
    private volatile WaitNode waiters;

    //获取运行结果获抛出取消异常获抛出运行异常
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }

    //使用callable的构造方法
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    //使用Runnable的构造方法
    public FutureTask(Runnable runnable, V result) {
        //这儿会将runnable封装为callable
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

    //判断任务是否已经取消 中断中或者已经中断都会认为是已经取消
    public boolean isCancelled() {
        return state >= CANCELLED;
    }
    
    //判断任务是否已经在执行或者执行完成,只要不处于new状态都返回true
    public boolean isDone() {
        return state != NEW;
    }

    //中断任务
    public boolean cancel(boolean mayInterruptIfRunning) {
        //如果任务已经尚处于new状态,先尝试使用CAS将任务状态设置为取消或者中断中
        if (!(state == NEW &&
UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        //如果要中断则进行中断
        try {    // in case call to interrupt throws exception
            if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally { // final state
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
            //进行收尾工作
            finishCompletion();
        }
        return true;
    }

    //获取运行结果
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        //如果尚未完成则等待完成,不带超时
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }

    //获取运行结果,带超时
    public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }

    //当任务完成的回调方法
    protected void done() { }

    //设置运行结果和运行状态,这是正确运行的情况
    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

    //设置运行结果和运行状态,这是抛出异常的情况
    protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }

    //运行任务 核心
    public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

    //运行,不设置结果,然后进行状态重置
    protected boolean runAndReset() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return false;
        boolean ran = false;
        int s = state;
        try {
            Callable<V> c = callable;
            if (c != null && s == NEW) {
                try {
                    c.call(); // don't set result
                    ran = true;
                } catch (Throwable ex) {
                    setException(ex);
                }
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
        return ran && s == NEW;
    }

    //检测可能的中断
    private void handlePossibleCancellationInterrupt(int s) {
        // It is possible for our interrupter to stall before getting a
        // chance to interrupt us.  Let's spin-wait patiently.
        if (s == INTERRUPTING)
            while (state == INTERRUPTING)
                Thread.yield(); // wait out pending interrupt

        // assert state == INTERRUPTED;

        // We want to clear any interrupt we may have received from
        // cancel(true).  However, it is permissible to use interrupts
        // as an independent mechanism for a task to communicate with
        // its caller, and there is no way to clear only the
        // cancellation interrupt.
        //
        // Thread.interrupted();
    }

    //任务结果等待节点
    static final class WaitNode {
        volatile Thread thread;
        volatile WaitNode next;
        WaitNode() { thread = Thread.currentThread(); }
    }

    //结束运行 依次唤醒等待get结果的节点
    private void finishCompletion() {
        // assert state > COMPLETING;
        for (WaitNode q; (q = waiters) != null;) {
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                for (;;) {
                    Thread t = q.thread;
                    if (t != null) {
                        q.thread = null;
                        LockSupport.unpark(t);
                    }
                    WaitNode next = q.next;
                    if (next == null)
                        break;
                    q.next = null; // unlink to help gc
                    q = next;
                }
                break;
            }
        }

        done();

        callable = null;        // to reduce footprint
    }

    // 结束运行 依次唤醒等待get结果的节点 带超时机制
    private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        for (;;) {
            if (Thread.interrupted()) {
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            else if (q == null)
                q = new WaitNode();
            else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                LockSupport.parkNanos(this, nanos);
            }
            else
                LockSupport.park(this);
        }
    }

    //移除等待节点
    private void removeWaiter(WaitNode node) {
        if (node != null) {
            node.thread = null;
            retry:
            for (;;) {          // restart on removeWaiter race
                for (WaitNode pred = null, q = waiters, s; q != null; q = s) {
                    s = q.next;
                    if (q.thread != null)
                        pred = q;
                    else if (pred != null) {
                        pred.next = s;
                        if (pred.thread == null) // check for race
                            continue retry;
                    }
                    else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                          q, s))
                        continue retry;
                }
                break;
            }
        }
    }



    // Unsafe 调用逻辑和本类相关字段的偏移位 cas和内存屏障需要
    private static final sun.misc.Unsafe UNSAFE;
    private static final long stateOffset;
    private static final long runnerOffset;
    private static final long waitersOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = FutureTask.class;
            stateOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("state"));
            runnerOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("runner"));
            waitersOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("waiters"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

  }

八.执行服务的抽象实现AbstractExecutorService
   该类实现自ExecutorService的大部分方法,但未实现核心的几个execute,shutdown等方法,源码如下:
   public abstract class AbstractExecutorService implements ExecutorService {

    //包装runnable形成一个具体的执行任务
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

    //包装callable 形成一个具体的执行任务
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

    //提交Runnable任务
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    //提交runable任务
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

    //提交callable任务
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

    //invokeany的核心
    private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
                              boolean timed, long nanos)
        throws InterruptedException, ExecutionException, TimeoutException {
       .....使用ExecutorCompletionService实现直到一个任务执行完成就返回的逻辑
    }

      //批量执行任务,直到一个任务执行完成
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException {
        try {
            return doInvokeAny(tasks, false, 0);
        } catch (TimeoutException cannotHappen) {
            assert false;
            return null;
        }
    }

    //批量执行任务,直到一个任务执行完成 带超时
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                           long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        return doInvokeAny(tasks, true, unit.toNanos(timeout));
    }

    //批量执行任务,直到全部运行完成
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
      ......
    }

    //批量执行任务,直到全部运行完成,带超时机制
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                         long timeout, TimeUnit unit)
        throws InterruptedException {
      ......
    }

}

九.线程池核心ThreadPoolExecutor
   该类继承自AbstractExecutorService,实现了所有的抽象方法,是线程池核心中的核心
   public class ThreadPoolExecutor extends AbstractExecutorService {
     .......
   }
   源码解析:https://user.qzone.qq.com/834171100/blog/1503996829
   几个重要参数:
   1.corePoolSize:线程池核心线程数,默认情况下,核心线程会在线程池中一直存活,即使它们处于闲置状态。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程在等待新任务到来时会有超时策略,这个时间间隔由keepAliveTime所指定的时长后,核心线程就会被终止。
   2.maximumPoolSize:线程池所能容纳的最大线程数,当活动线程达到这个数值后,后续的新任务将被阻塞。
   3.keepAliveTime:非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收。当ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true时,keepAliveTime同样会作用于核心线程。
   4.unit:keepAliveTime 参数的时间单位。
   5.workQueue:执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
   6.threadFactory:执行程序创建新线程时使用的工厂。
   7.handler:由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。

十.任务执行服务的工程和辅助方法类Executors
   暂时只看几个工厂方法:
   public class Executors {

    //固定线程数量的执行服务
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

    //基于工作窃取原理的任务执行服务(fork-join) parallelism是线程数量
    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

    //基于工作窃取原理的任务执行服务(fork-join) 线程数量等于cpu核数
    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

    //固定线程数量的执行服务
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

    //单线程的任务执行服务
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

    //单线程的执行服务
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

   //工作线程带缓存的任务执行服务 只是最大数量设置为了Integer.MAX_VALUE,以及过期时间设置长了而已
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

     //工作线程带缓存的任务执行服务 只是最大数量设置为了Integer.MAX_VALUE,以及过期时间设置长了而已
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

    //单线程的定时任务执行服务
    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }

    //单线程的定时任务执行服务
    public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1, threadFactory));
    }

    //定时任务执行服务
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

    //定时任务执行服务
    public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }
    ......
    }

十一.用于批量任务执行管理的CompletionService和ExecutorCompletionService
    CompletionService接口定义了一组带相同返回值的任务的管理接口,可以最快的获取这些批量任务的执行结果,ExecutorService.invokeAny就使用了这个技术!
    源码如下:
    public interface CompletionService<V> {
        //提交任务
        Future<V> submit(Callable<V> task);

        //提交任务
        Future<V> submit(Runnable task, V result);

        //获取一个执行结果(并移除) 阻塞
        Future<V> take() throws InterruptedException;

        //获取一个执行结果(并移除) 不阻塞
        Future<V> poll();

        //获取一个执行结果(并移除) 带超时
        Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
     }
    ExecutorCompletionService类是CompletionService接口的实现
    ExecutorCompletionService内部管理者一个已完成任务的阻塞队列,其引用了一个Executor用来执行任务,submit()方法最终会委托给内部的executor去执行任务,take/poll方法的工作都委托给内部的已完成任务阻塞队列
    如果阻塞队列中有已完成的任务, take方法就返回任务的结果, 否则阻塞等待任务完成
    源码如下:
    public class ExecutorCompletionService<V> implements CompletionService<V> {
            //被包装Executor
            private final Executor executor;
            //如果executor是AbstractExecutorService就为它
            private final AbstractExecutorService aes;
            //完成任务的阻塞队列
            private final BlockingQueue<Future<V>> completionQueue;

            /**
             * 扩展FutureTask,重写了其done方法用于将完成任务放置在阻塞队列!!!
             */
            private class QueueingFuture extends FutureTask<Void> {
                QueueingFuture(RunnableFuture<V> task) {
                    super(task, null);
                    this.task = task;
                }
                protected void done() { completionQueue.add(task); }
                private final Future<V> task;
            }

            //包装callable
            private RunnableFuture<V> newTaskFor(Callable<V> task) {
                if (aes == null)
                    return new FutureTask<V>(task);
                else
                    return aes.newTaskFor(task);
            }

            //包装Runnable
            private RunnableFuture<V> newTaskFor(Runnable task, V result) {
                if (aes == null)
                    return new FutureTask<V>(task, result);
                else
                    return aes.newTaskFor(task, result);
            }

            
            public ExecutorCompletionService(Executor executor) {
                if (executor == null)
                    throw new NullPointerException();
                this.executor = executor;
                this.aes = (executor instanceof AbstractExecutorService) ?
                    (AbstractExecutorService) executor : null;
this.completionQueue = new LinkedBlockingQueue<Future<V>>();
            }

           
            public ExecutorCompletionService(Executor executor,
                                             BlockingQueue<Future<V>> completionQueue) {
                if (executor == null || completionQueue == null)
                    throw new NullPointerException();
                this.executor = executor;
                this.aes = (executor instanceof AbstractExecutorService) ?
                    (AbstractExecutorService) executor : null;
this.completionQueue = completionQueue;
            }

            //提交任务
            public Future<V> submit(Callable<V> task) {
                if (task == null) throw new NullPointerException();
                RunnableFuture<V> f = newTaskFor(task);
                executor.execute(new QueueingFuture(f));
                return f;
            }

            //提交任务
            public Future<V> submit(Runnable task, V result) {
                if (task == null) throw new NullPointerException();
                RunnableFuture<V> f = newTaskFor(task, result);
                executor.execute(new QueueingFuture(f));
                return f;
            }

            //获取结果带阻塞
            public Future<V> take() throws InterruptedException {
                return completionQueue.take();
            }

             //获取结果
            public Future<V> poll() {
                return completionQueue.poll();
            }

            //获取结果 带超时
            public Future<V> poll(long timeout, TimeUnit unit)
                    throws InterruptedException {
                return completionQueue.poll(timeout, unit);
            }

        }
       ExecutorCompletionService主要用与管理一组带相同返回结果类型的异步任务 (有结果的任务, 任务完成后要处理结果)
       more:https://user.qzone.qq.com/834171100/blog/1516002218


原文地址:http://blog.csdn.net/aitangyong/article/details/38315885

JDK提供了写锁接口ReadWriteLock和它的实现ReentrantReadWriteLock。要实现一个读写锁,需要考虑很多细节,其中之一就是锁升级和锁降级的问题。什么是升级和降级呢?ReadWriteLock的javadoc有一段话:
[java] view plain copy
  1. Can the write lock be downgraded to a read lock without allowing an intervening writer? Can a read lock be upgraded to a write lock,in preference to other waiting readers or writers?  
锁降级:从写锁变成读锁;锁升级:从读锁变成写锁。读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。
如下代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的。
[java] view plain copy
  1. ReadWriteLock rtLock = new ReentrantReadWriteLock();  
  2. rtLock.readLock().lock();  
  3. System.out.println("get readLock.");  
  4. rtLock.writeLock().lock();  
  5. System.out.println("blocking");  
ReentrantReadWriteLock支持锁降级,如下代码不会产生死锁。
[java] view plain copy
  1. ReadWriteLock rtLock = new ReentrantReadWriteLock();  
  2. rtLock.writeLock().lock();  
  3. System.out.println("writeLock");  
  4.   
  5. rtLock.readLock().lock();  
  6. System.out.println("get read lock");  
这段代码虽然不会导致死锁,但没有正确的释放锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。锁的释放和获取可以看下:可重入锁的获取和释放需要注意的一点儿事
[java] view plain copy
  1. final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();  
  2. Thread wt = new Thread(new Runnable()  
  3. {  
  4.   
  5.     @Override  
  6.     public void run()  
  7.     {  
  8.         readWriteLock.writeLock().lock();  
  9.         System.out.println("writeLock");  
  10.   
  11.         readWriteLock.readLock().lock();  
  12.         System.out.println("readLock");  
  13.         readWriteLock.readLock().unlock();  
  14.   
  15.         System.out.println("block");  
  16.     }  
  17. });  
  18.   
  19. wt.start();  
  20.   
  21. Thread.sleep(100);  
  22.   
  23. System.out.println("main blocking.");  
  24. readWriteLock.readLock().lock();  



JAVA8源码
/**
 * A bounded {@linkplain BlockingQueue blocking queue} backed by an
 * array.  This queue orders elements FIFO (first-in-first-out).  The
 * <em>head</em> of the queue is that element that has been on the
 * queue the longest time.  The <em>tail</em> of the queue is that
 * element that has been on the queue the shortest time. New elements
 * are inserted at the tail of the queue, and the queue retrieval
 * operations obtain elements at the head of the queue.
 *
 * <p>This is a classic &quot;bounded buffer&quot;, in which a
 * fixed-sized array holds elements inserted by producers and
 * extracted by consumers.  Once created, the capacity cannot be
 * changed.  Attempts to {@code put} an element into a full queue
 * will result in the operation blocking; attempts to {@code take} an
 * element from an empty queue will similarly block.
 *
 * <p>This class supports an optional fairness policy for ordering
 * waiting producer and consumer threads.  By default, this ordering
 * is not guaranteed. However, a queue constructed with fairness set
 * to {@code true} grants threads access in FIFO order. Fairness
 * generally decreases throughput but reduces variability and avoids
 * starvation.
 *
 * <p>This class and its iterator implement all of the
 * <em>optional</em> methods of the {@link Collection} and {@link
 * Iterator} interfaces.
 *
 * <p>This class is a member of the
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 * Java Collections Framework</a>.
 *
 * @since 1.5
 * @author Doug Lea
 * @param <E> the type of elements held in this collection
 */
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    /**
     * Serialization ID. This class relies on default serialization
     * even for the items array, which is default-serialized, even if
     * it is empty. Otherwise it could not be declared final, which is
     * necessary here.
     */
    private static final long serialVersionUID = -817911632652898426L;
    /** The queued items */
    final Object[] items;
    /** items index for next take, poll, peek or remove */
    int takeIndex;
    /** items index for next put, offer, or add */
    int putIndex;
    /** Number of elements in the queue */
    int count;
    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */
    /** Main lock guarding all access */
    final ReentrantLock lock;
    /** Condition for waiting takes */
    private final Condition notEmpty;
    /** Condition for waiting puts */
    private final Condition notFull;
    /**
     * Shared state for currently active iterators, or null if there
     * are known not to be any.  Allows queue operations to update
     * iterator state.
     */
    transient Itrs itrs = null;
    // Internal helper methods
    /**
     * Circularly decrement i.
     */
    final int dec(int i) {
        return ((i == 0) ? items.length : i) - 1;
    }
    /**
     * Returns item at index i.
     */
    @SuppressWarnings("unchecked")
    final E itemAt(int i) {
        return (E) items[i];
    }
    /**
     * Throws NullPointerException if argument is null.
     *
     * @param v the element
     */
    private static void checkNotNull(Object v) {
        if (v == null)
            throw new NullPointerException();
    }
    /**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }
    /**
     * Extracts element at current take position, advances, and signals.
     * Call only when holding lock.
     */
    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }
    /**
     * Deletes item at array index removeIndex.
     * Utility for remove(Object) and iterator.remove.
     * Call only when holding lock.
     */
    void removeAt(final int removeIndex) {
        // assert lock.getHoldCount() == 1;
        // assert items[removeIndex] != null;
        // assert removeIndex >= 0 && removeIndex < items.length;
        final Object[] items = this.items;
        if (removeIndex == takeIndex) {
            // removing front item; just advance
            items[takeIndex] = null;
            if (++takeIndex == items.length)
                takeIndex = 0;
            count--;
            if (itrs != null)
                itrs.elementDequeued();
        } else {
            // an "interior" remove
            // slide over all others up through putIndex.
            final int putIndex = this.putIndex;
            for (int i = removeIndex;;) {
                int next = i + 1;
                if (next == items.length)
                    next = 0;
                if (next != putIndex) {
                    items[i] = items[next];
                    i = next;
                } else {
                    items[i] = null;
                    this.putIndex = i;
                    break;
                }
            }
            count--;
            if (itrs != null)
                itrs.removedAt(removeIndex);
        }
        notFull.signal();
    }
    /**
     * Creates an {@code ArrayBlockingQueue} with the given (fixed)
     * capacity and default access policy.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity < 1}
     */
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
    /**
     * Creates an {@code ArrayBlockingQueue} with the given (fixed)
     * capacity and the specified access policy.
     *
     * @param capacity the capacity of this queue
     * @param fair if {@code true} then queue accesses for threads blocked
     *        on insertion or removal, are processed in FIFO order;
     *        if {@code false} the access order is unspecified.
     * @throws IllegalArgumentException if {@code capacity < 1}
     */
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull lock.newCondition();
    }
    /**
     * Creates an {@code ArrayBlockingQueue} with the given (fixed)
     * capacity, the specified access policy and initially containing the
     * elements of the given collection,
     * added in traversal order of the collection's iterator.
     *
     * @param capacity the capacity of this queue
     * @param fair if {@code true} then queue accesses for threads blocked
     *        on insertion or removal, are processed in FIFO order;
     *        if {@code false} the access order is unspecified.
     * @param c the collection of elements to initially contain
     * @throws IllegalArgumentException if {@code capacity} is less than
     *         {@code c.size()}, or less than 1.
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);
        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }
    /**
     * Inserts the specified element at the tail of this queue if it is
     * possible to do so immediately without exceeding the queue's capacity,
     * returning {@code true} upon success and throwing an
     * {@code IllegalStateException} if this queue is full.
     *
     * @param e the element to add
     * @return {@code true} (as specified by {@link Collection#add})
     * @throws IllegalStateException if this queue is full
     * @throws NullPointerException if the specified element is null
     */
    public boolean add(E e) {
        return super.add(e);
    }
    /**
     * Inserts the specified element at the tail of this queue if it is
     * possible to do so immediately without exceeding the queue's capacity,
     * returning {@code true} upon success and {@code false} if this queue
     * is full.  This method is generally preferable to method {@link #add},
     * which can fail to insert an element only by throwing an exception.
     *
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
    /**
     * Inserts the specified element at the tail of this queue, waiting
     * for space to become available if the queue is full.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    /**
     * Inserts the specified element at the tail of this queue, waiting
     * up to the specified wait time for space to become available if
     * the queue is full.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }
    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return itemAt(takeIndex); // null when queue is empty
        } finally {
            lock.unlock();
        }
    }
    // this doc comment is overridden to remove the reference to collections
    // greater in size than Integer.MAX_VALUE
    /**
     * Returns the number of elements in this queue.
     *
     * @return the number of elements in this queue
     */
    public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
    // this doc comment is a modified copy of the inherited doc comment,
    // without the reference to unlimited queues.
    /**
     * Returns the number of additional elements that this queue can ideally
     * (in the absence of memory or resource constraints) accept without
     * blocking. This is always equal to the initial capacity of this queue
     * less the current {@code size} of this queue.
     *
     * <p>Note that you <em>cannot</em> always tell if an attempt to insert
     * an element will succeed by inspecting {@code remainingCapacity}
     * because it may be the case that another thread is about to
     * insert or remove an element.
     */
    public int remainingCapacity() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return items.length - count;
        } finally {
            lock.unlock();
        }
    }
    /**
     * Removes a single instance of the specified element from this queue,
     * if it is present.  More formally, removes an element {@code e} such
     * that {@code o.equals(e)}, if this queue contains one or more such
     * elements.
     * Returns {@code true} if this queue contained the specified element
     * (or equivalently, if this queue changed as a result of the call).
     *
     * <p>Removal of interior elements in circular array based queues
     * is an intrinsically slow and disruptive operation, so should
     * be undertaken only in exceptional circumstances, ideally
     * only when the queue is known not to be accessible by other
     * threads.
     *
     * @param o element to be removed from this queue, if present
     * @return {@code true} if this queue changed as a result of the call
     */
    public boolean remove(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count > 0) {
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do {
                    if (o.equals(items[i])) {
                        removeAt(i);
                        return true;
                    }
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }
    /**
     * Returns {@code true} if this queue contains the specified element.
     * More formally, returns {@code true} if and only if this queue contains
     * at least one element {@code e} such that {@code o.equals(e)}.
     *
     * @param o object to be checked for containment in this queue
     * @return {@code true} if this queue contains the specified element
     */
    public boolean contains(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count > 0) {
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do {
                    if (o.equals(items[i]))
                        return true;
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }
    /**
     * Returns an array containing all of the elements in this queue, in
     * proper sequence.
     *
     * <p>The returned array will be "safe" in that no references to it are
     * maintained by this queue.  (In other words, this method must allocate
     * a new array).  The caller is thus free to modify the returned array.
     *
     * <p>This method acts as bridge between array-based and collection-based
     * APIs.
     *
     * @return an array containing all of the elements in this queue
     */
    public Object[] toArray() {
        Object[] a;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            final int count = this.count;
            a = new Object[count];
            int n = items.length - takeIndex;
            if (count <= n)
                System.arraycopy(items, takeIndex, a, 0, count);
            else {
                System.arraycopy(items, takeIndex, a, 0, n);
                System.arraycopy(items, 0, a, n, count - n);
            }
        } finally {
            lock.unlock();
        }
        return a;
    }
    /**
     * Returns an array containing all of the elements in this queue, in
     * proper sequence; the runtime type of the returned array is that of
     * the specified array.  If the queue fits in the specified array, it
     * is returned therein.  Otherwise, a new array is allocated with the
     * runtime type of the specified array and the size of this queue.
     *
     * <p>If this queue fits in the specified array with room to spare
     * (i.e., the array has more elements than this queue), the element in
     * the array immediately following the end of the queue is set to
     * {@code null}.
     *
     * <p>Like the {@link #toArray()} method, this method acts as bridge between
     * array-based and collection-based APIs.  Further, this method allows
     * precise control over the runtime type of the output array, and may,
     * under certain circumstances, be used to save allocation costs.
     *
     * <p>Suppose {@code x} is a queue known to contain only strings.
     * The following code can be used to dump the queue into a newly
     * allocated array of {@code String}:
     *
     *  <pre> {@code String[] y = x.toArray(new String[0]);}</pre>
     *
     * Note that {@code toArray(new Object[0])} is identical in function to
     * {@code toArray()}.
     *
     * @param a the array into which the elements of the queue are to
     *          be stored, if it is big enough; otherwise, a new array of the
     *          same runtime type is allocated for this purpose
     * @return an array containing all of the elements in this queue
     * @throws ArrayStoreException if the runtime type of the specified array
     *         is not a supertype of the runtime type of every element in
     *         this queue
     * @throws NullPointerException if the specified array is null
     */
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            final int count = this.count;
            final int len = a.length;
            if (len < count)
                a = (T[])java.lang.reflect.Array.newInstance(
                    a.getClass().getComponentType(), count);
            int n = items.length - takeIndex;
            if (count <= n)
                System.arraycopy(items, takeIndex, a, 0, count);
            else {
                System.arraycopy(items, takeIndex, a, 0, n);
                System.arraycopy(items, 0, a, n, count - n);
            }
            if (len > count)
                a[count] = null;
        } finally {
            lock.unlock();
        }
        return a;
    }
    public String toString() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int k = count;
            if (k == 0)
                return "[]";
            final Object[] items = this.items;
            StringBuilder sb = new StringBuilder();
            sb.append('[');
            for (int i = takeIndex; ; ) {
                Object e = items[i];
                sb.append(e == this ? "(this Collection)" : e);
                if (--k == 0)
                    return sb.append(']').toString();
                sb.append(',').append(' ');
                if (++i == items.length)
                    i = 0;
            }
        } finally {
            lock.unlock();
        }
    }
    /**
     * Atomically removes all of the elements from this queue.
     * The queue will be empty after this call returns.
     */
    public void clear() {
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int k = count;
            if (k > 0) {
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do {
                    items[i] = null;
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
                takeIndex = putIndex;
                count = 0;
                if (itrs != null)
                    itrs.queueIsEmpty();
                for (; k > 0 && lock.hasWaiters(notFull); k--)
                    notFull.signal();
            }
        } finally {
            lock.unlock();
        }
    }
    /**
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     * @throws IllegalArgumentException      {@inheritDoc}
     */
    public int drainTo(Collection<? super E> c) {
        return drainTo(c, Integer.MAX_VALUE);
    }
    /**
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     * @throws IllegalArgumentException      {@inheritDoc}
     */
    public int drainTo(Collection<? super E> c, int maxElements) {
        checkNotNull(c);
        if (c == this)
            throw new IllegalArgumentException();
        if (maxElements <= 0)
            return 0;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int n = Math.min(maxElements, count);
            int take = takeIndex;
            int i = 0;
            try {
                while (i < n) {
                    @SuppressWarnings("unchecked")
                    E x = (E) items[take];
                    c.add(x);
                    items[take] = null;
                    if (++take == items.length)
                        take = 0;
                    i++;
                }
                return n;
            } finally {
                // Restore invariants even if c.add() threw
                if (i > 0) {
                    count -= i;
                    takeIndex = take;
                    if (itrs != null) {
                        if (count == 0)
                            itrs.queueIsEmpty();
                        else if (i > take)
                            itrs.takeIndexWrapped();
                    }
                    for (; i > 0 && lock.hasWaiters(notFull); i--)
                        notFull.signal();
                }
            }
        } finally {
            lock.unlock();
        }
    }
    /**
     * Returns an iterator over the elements in this queue in proper sequence.
     * The elements will be returned in order from first (head) to last (tail).
     *
     * <p>The returned iterator is
     * <a href="package-summary.html#Weakly"><i>weakly consistent</i></a>.
     *
     * @return an iterator over the elements in this queue in proper sequence
     */
    public Iterator<E> iterator() {
        return new Itr();
    }
    /**
     * Shared data between iterators and their queue, allowing queue
     * modifications to update iterators when elements are removed.
     *
     * This adds a lot of complexity for the sake of correctly
     * handling some uncommon operations, but the combination of
     * circular-arrays and supporting interior removes (i.e., those
     * not at head) would cause iterators to sometimes lose their
     * places and/or (re)report elements they shouldn't.  To avoid
     * this, when a queue has one or more iterators, it keeps iterator
     * state consistent by:
     *
     * (1) keeping track of the number of "cycles", that is, the
     *     number of times takeIndex has wrapped around to 0.
     * (2) notifying all iterators via the callback removedAt whenever
     *     an interior element is removed (and thus other elements may
     *     be shifted).
     *
     * These suffice to eliminate iterator inconsistencies, but
     * unfortunately add the secondary responsibility of maintaining
     * the list of iterators.  We track all active iterators in a
     * simple linked list (accessed only when the queue's lock is
     * held) of weak references to Itr.  The list is cleaned up using
     * 3 different mechanisms:
     *
     * (1) Whenever a new iterator is created, do some O(1) checking for
     *     stale list elements.
     *
     * (2) Whenever takeIndex wraps around to 0, check for iterators
     *     that have been unused for more than one wrap-around cycle.
     *
     * (3) Whenever the queue becomes empty, all iterators are notified
     *     and this entire data structure is discarded.
     *
     * So in addition to the removedAt callback that is necessary for
     * correctness, iterators have the shutdown and takeIndexWrapped
     * callbacks that help remove stale iterators from the list.
     *
     * Whenever a list element is examined, it is expunged if either
     * the GC has determined that the iterator is discarded, or if the
     * iterator reports that it is "detached" (does not need any
     * further state updates).  Overhead is maximal when takeIndex
     * never advances, iterators are discarded before they are
     * exhausted, and all removals are interior removes, in which case
     * all stale iterators are discovered by the GC.  But even in this
     * case we don't increase the amortized complexity.
     *
     * Care must be taken to keep list sweeping methods from
     * reentrantly invoking another such method, causing subtle
     * corruption bugs.
     */
    class Itrs {
        /**
         * Node in a linked list of weak iterator references.
         */
        private class Node extends WeakReference<Itr> {
            Node next;
            Node(Itr iterator, Node next) {
                super(iterator);
                this.next = next;
            }
        }
        /** Incremented whenever takeIndex wraps around to 0 */
        int cycles = 0;
        /** Linked list of weak iterator references */
        private Node head;
        /** Used to expunge stale iterators */
        private Node sweeper = null;
        private static final int SHORT_SWEEP_PROBES = 4;
        private static final int LONG_SWEEP_PROBES = 16;
        Itrs(Itr initial) {
            register(initial);
        }
        /**
         * Sweeps itrs, looking for and expunging stale iterators.
         * If at least one was found, tries harder to find more.
         * Called only from iterating thread.
         *
         * @param tryHarder whether to start in try-harder mode, because
         * there is known to be at least one iterator to collect
         */
        void doSomeSweeping(boolean tryHarder) {
            // assert lock.getHoldCount() == 1;
            // assert head != null;
            int probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES;
            Node o, p;
            final Node sweeper = this.sweeper;
            boolean passedGo;   // to limit search to one full sweep
            if (sweeper == null) {
                o = null;
                p = head;
                passedGo = true;
            } else {
                o = sweeper;
                p = o.next;
                passedGo = false;
            }
            for (; probes > 0; probes--) {
                if (p == null) {
                    if (passedGo)
                        break;
                    o = null;
                    p = head;
                    passedGo = true;
                }
                final Itr it = p.get();
                final Node next = p.next;
                if (it == null || it.isDetached()) {
                    // found a discarded/exhausted iterator
                    probes = LONG_SWEEP_PROBES; // "try harder"
                    // unlink p
                    p.clear();
                    p.next = null;
                    if (o == null) {
                        head = next;
                        if (next == null) {
                            // We've run out of iterators to track; retire
                            itrs = null;
                            return;
                        }
                    }
                    else
                        o.next = next;
                } else {
                    o = p;
                }
                p = next;
            }
            this.sweeper = (p == null) ? null : o;
        }
        /**
         * Adds a new iterator to the linked list of tracked iterators.
         */
        void register(Itr itr) {
            // assert lock.getHoldCount() == 1;
            head = new Node(itr, head);
        }
        /**
         * Called whenever takeIndex wraps around to 0.
         *
         * Notifies all iterators, and expunges any that are now stale.
         */
        void takeIndexWrapped() {
            // assert lock.getHoldCount() == 1;
            cycles++;
            for (Node o = null, p = head; p != null;) {
                final Itr it = p.get();
                final Node next = p.next;
                if (it == null || it.takeIndexWrapped()) {
                    // unlink p
                    // assert it == null || it.isDetached();
                    p.clear();
                    p.next = null;
                    if (o == null)
                        head = next;
                    else
                        o.next = next;
                } else {
                    o = p;
                }
                p = next;
            }
            if (head == null)   // no more iterators to track
                itrs = null;
        }
        /**
         * Called whenever an interior remove (not at takeIndex) occurred.
         *
         * Notifies all iterators, and expunges any that are now stale.
         */
        void removedAt(int removedIndex) {
            for (Node o = null, p = head; p != null;) {
                final Itr it = p.get();
                final Node next = p.next;
                if (it == null || it.removedAt(removedIndex)) {
                    // unlink p
                    // assert it == null || it.isDetached();
                    p.clear();
                    p.next = null;
                    if (o == null)
                        head = next;
                    else
                        o.next = next;
                } else {
                    o = p;
                }
                p = next;
            }
            if (head == null)   // no more iterators to track
                itrs = null;
        }
        /**
         * Called whenever the queue becomes empty.
         *
         * Notifies all active iterators that the queue is empty,
         * clears all weak refs, and unlinks the itrs datastructure.
         */
        void queueIsEmpty() {
            // assert lock.getHoldCount() == 1;
            for (Node p = head; p != null; p = p.next) {
                Itr it = p.get();
                if (it != null) {
                    p.clear();
                    it.shutdown();
                }
            }
            head = null;
            itrs = null;
        }
        /**
         * Called whenever an element has been dequeued (at takeIndex).
         */
        void elementDequeued() {
            // assert lock.getHoldCount() == 1;
            if (count == 0)
                queueIsEmpty();
            else if (takeIndex == 0)
                takeIndexWrapped();
        }
    }
    /**
     * Iterator for ArrayBlockingQueue.
     *
     * To maintain weak consistency with respect to puts and takes, we
     * read ahead one slot, so as to not report hasNext true but then
     * not have an element to return.
     *
     * We switch into "detached" mode (allowing prompt unlinking from
     * itrs without help from the GC) when all indices are negative, or
     * when hasNext returns false for the first time.  This allows the
     * iterator to track concurrent updates completely accurately,
     * except for the corner case of the user calling Iterator.remove()
     * after hasNext() returned false.  Even in this case, we ensure
     * that we don't remove the wrong element by keeping track of the
     * expected element to remove, in lastItem.  Yes, we may fail to
     * remove lastItem from the queue if it moved due to an interleaved
     * interior remove while in detached mode.
     */
    private class Itr implements Iterator<E> {
        /** Index to look for new nextItem; NONE at end */
        private int cursor;
        /** Element to be returned by next call to next(); null if none */
        private E nextItem;
        /** Index of nextItem; NONE if none, REMOVED if removed elsewhere */
        private int nextIndex;
        /** Last element returned; null if none or not detached. */
        private E lastItem;
        /** Index of lastItem, NONE if none, REMOVED if removed elsewhere */
        private int lastRet;
        /** Previous value of takeIndex, or DETACHED when detached */
        private int prevTakeIndex;
        /** Previous value of iters.cycles */
        private int prevCycles;
        /** Special index value indicating "not available" or "undefined" */
        private static final int NONE = -1;
        /**
         * Special index value indicating "removed elsewhere", that is,
         * removed by some operation other than a call to this.remove().
         */
        private static final int REMOVED = -2;
        /** Special value for prevTakeIndex indicating "detached mode" */
        private static final int DETACHED = -3;
        Itr() {
            // assert lock.getHoldCount() == 0;
            lastRet = NONE;
            final ReentrantLock lock = ArrayBlockingQueue.this.lock;
            lock.lock();
            try {
                if (count == 0) {
                    // assert itrs == null;
                    cursor = NONE;
                    nextIndex = NONE;
                    prevTakeIndex = DETACHED;
                } else {
                    final int takeIndex = ArrayBlockingQueue.this.takeIndex;
                    prevTakeIndex = takeIndex;
                    nextItem = itemAt(nextIndex = takeIndex);
                    cursor = incCursor(takeIndex);
                    if (itrs == null) {
                        itrs = new Itrs(this);
                    } else {
                        itrs.register(this); // in this order
                        itrs.doSomeSweeping(false);
                    }
                    prevCycles = itrs.cycles;
                    // assert takeIndex >= 0;
                    // assert prevTakeIndex == takeIndex;
                    // assert nextIndex >= 0;
                    // assert nextItem != null;
                }
            } finally {
                lock.unlock();
            }
        }
        boolean isDetached() {
            // assert lock.getHoldCount() == 1;
            return prevTakeIndex < 0;
        }
        private int incCursor(int index) {
            // assert lock.getHoldCount() == 1;
            if (++index == items.length)
                index = 0;
            if (index == putIndex)
                index = NONE;
            return index;
        }
        /**
         * Returns true if index is invalidated by the given number of
         * dequeues, starting from prevTakeIndex.
         */
        private boolean invalidated(int index, int prevTakeIndex,
                                    long dequeues, int length) {
            if (index < 0)
                return false;
            int distance = index - prevTakeIndex;
            if (distance < 0)
                distance += length;
            return dequeues > distance;
        }
        /**
         * Adjusts indices to incorporate all dequeues since the last
         * operation on this iterator.  Call only from iterating thread.
         */
        private void incorporateDequeues() {
            // assert lock.getHoldCount() == 1;
            // assert itrs != null;
            // assert !isDetached();
            // assert count > 0;
            final int cycles = itrs.cycles;
            final int takeIndex = ArrayBlockingQueue.this.takeIndex;
            final int prevCycles = this.prevCycles;
            final int prevTakeIndex = this.prevTakeIndex;
            if (cycles != prevCycles || takeIndex != prevTakeIndex) {
                final int len = items.length;
                // how far takeIndex has advanced since the previous
                // operation of this iterator
                long dequeues = (cycles - prevCycles) * len
                    + (takeIndex - prevTakeIndex);
                // Check indices for invalidation
                if (invalidated(lastRet, prevTakeIndex, dequeues, len))
                    lastRet = REMOVED;
                if (invalidated(nextIndex, prevTakeIndex, dequeues, len))
                    nextIndex = REMOVED;
                if (invalidated(cursor, prevTakeIndex, dequeues, len))
                    cursor = takeIndex;
                if (cursor < 0 && nextIndex < 0 && lastRet < 0)
                    detach();
                else {
                    this.prevCycles = cycles;
                    this.prevTakeIndex = takeIndex;
                }
            }
        }
        /**
         * Called when itrs should stop tracking this iterator, either
         * because there are no more indices to update (cursor < 0 &&
         * nextIndex < 0 && lastRet < 0) or as a special exception, when
         * lastRet >= 0, because hasNext() is about to return false for the
         * first time.  Call only from iterating thread.
         */
        private void detach() {
            // Switch to detached mode
            // assert lock.getHoldCount() == 1;
            // assert cursor == NONE;
            // assert nextIndex < 0;
            // assert lastRet < 0 || nextItem == null;
            // assert lastRet < 0 ^ lastItem != null;
            if (prevTakeIndex >= 0) {
                // assert itrs != null;
                prevTakeIndex = DETACHED;
                // try to unlink from itrs (but not too hard)
                itrs.doSomeSweeping(true);
            }
        }
        /**
         * For performance reasons, we would like not to acquire a lock in
         * hasNext in the common case.  To allow for this, we only access
         * fields (i.e. nextItem) that are not modified by update operations
         * triggered by queue modifications.
         */
        public boolean hasNext() {
            // assert lock.getHoldCount() == 0;
            if (nextItem != null)
                return true;
            noNext();
            return false;
        }
        private void noNext() {
            final ReentrantLock lock = ArrayBlockingQueue.this.lock;
            lock.lock();
            try {
                // assert cursor == NONE;
                // assert nextIndex == NONE;
                if (!isDetached()) {
                    // assert lastRet >= 0;
                    incorporateDequeues(); // might update lastRet
                    if (lastRet >= 0) {
                        lastItem = itemAt(lastRet);
                        // assert lastItem != null;
                        detach();
                    }
                }
                // assert isDetached();
                // assert lastRet < 0 ^ lastItem != null;
            } finally {
                lock.unlock();
            }
        }
        public E next() {
            // assert lock.getHoldCount() == 0;
            final E x = nextItem;
            if (x == null)
                throw new NoSuchElementException();
            final ReentrantLock lock = ArrayBlockingQueue.this.lock;
            lock.lock();
            try {
                if (!isDetached())
                    incorporateDequeues();
                // assert nextIndex != NONE;
                // assert lastItem == null;
                lastRet = nextIndex;
                final int cursor = this.cursor;
                if (cursor >= 0) {
                    nextItem = itemAt(nextIndex = cursor);
                    // assert nextItem != null;
                    this.cursor = incCursor(cursor);
                } else {
                    nextIndex = NONE;
                    nextItem = null;
                }
            } finally {
                lock.unlock();
            }
            return x;
        }
        public void remove() {
            // assert lock.getHoldCount() == 0;
            final ReentrantLock lock = ArrayBlockingQueue.this.lock;
            lock.lock();
            try {
                if (!isDetached())
                    incorporateDequeues(); // might update lastRet or detach
                final int lastRet = this.lastRet;
                this.lastRet = NONE;
                if (lastRet >= 0) {
                    if (!isDetached())
                        removeAt(lastRet);
                    else {
                        final E lastItem = this.lastItem;
                        // assert lastItem != null;
                        this.lastItem = null;
                        if (itemAt(lastRet) == lastItem)
                            removeAt(lastRet);
                    }
                } else if (lastRet == NONE)
                    throw new IllegalStateException();
                // else lastRet == REMOVED and the last returned element was
                // previously asynchronously removed via an operation other
                // than this.remove(), so nothing to do.
                if (cursor < 0 && nextIndex < 0)
                    detach();
            } finally {
                lock.unlock();
                // assert lastRet == NONE;
                // assert lastItem == null;
            }
        }
        /**
         * Called to notify the iterator that the queue is empty, or that it
         * has fallen hopelessly behind, so that it should abandon any
         * further iteration, except possibly to return one more element
         * from next(), as promised by returning true from hasNext().
         */
        void shutdown() {
            // assert lock.getHoldCount() == 1;
            cursor = NONE;
            if (nextIndex >= 0)
                nextIndex = REMOVED;
            if (lastRet >= 0) {
                lastRet = REMOVED;
                lastItem = null;
            }
            prevTakeIndex = DETACHED;
            // Don't set nextItem to null because we must continue to be
            // able to return it on next().
            //
            // Caller will unlink from itrs when convenient.
        }
        private int distance(int index, int prevTakeIndex, int length) {
            int distance = index - prevTakeIndex;
            if (distance < 0)
                distance += length;
            return distance;
        }
        /**
         * Called whenever an interior remove (not at takeIndex) occurred.
         *
         * @return true if this iterator should be unlinked from itrs
         */
        boolean removedAt(int removedIndex) {
            // assert lock.getHoldCount() == 1;
            if (isDetached())
                return true;
            final int cycles = itrs.cycles;
            final int takeIndex = ArrayBlockingQueue.this.takeIndex;
            final int prevCycles = this.prevCycles;
            final int prevTakeIndex = this.prevTakeIndex;
            final int len = items.length;
            int cycleDiff = cycles - prevCycles;
            if (removedIndex < takeIndex)
                cycleDiff++;
            final int removedDistance =
                (cycleDiff * len) + (removedIndex - prevTakeIndex);
            // assert removedDistance >= 0;
            int cursor = this.cursor;
            if (cursor >= 0) {
                int x = distance(cursor, prevTakeIndex, len);
                if (x == removedDistance) {
                    if (cursor == putIndex)
                        this.cursor = cursor = NONE;
                }
                else if (x > removedDistance) {
                    // assert cursor != prevTakeIndex;
                    this.cursor = cursor = dec(cursor);
                }
            }
            int lastRet = this.lastRet;
            if (lastRet >= 0) {
                int x = distance(lastRet, prevTakeIndex, len);
                if (x == removedDistance)
                    this.lastRet = lastRet = REMOVED;
                else if (x > removedDistance)
                    this.lastRet = lastRet = dec(lastRet);
            }
            int nextIndex = this.nextIndex;
            if (nextIndex >= 0) {
                int x = distance(nextIndex, prevTakeIndex, len);
                if (x == removedDistance)
                    this.nextIndex = nextIndex = REMOVED;
                else if (x > removedDistance)
                    this.nextIndex = nextIndex = dec(nextIndex);
            }
            else if (cursor < 0 && nextIndex < 0 && lastRet < 0) {
                this.prevTakeIndex = DETACHED;
                return true;
            }
            return false;
        }
        /**
         * Called whenever takeIndex wraps around to zero.
         *
         * @return true if this iterator should be unlinked from itrs
         */
        boolean takeIndexWrapped() {
            // assert lock.getHoldCount() == 1;
            if (isDetached())
                return true;
            if (itrs.cycles - prevCycles > 1) {
                // All the elements that existed at the time of the last
                // operation are gone, so abandon further iteration.
                shutdown();
                return true;
            }
            return false;
        }
//         /** Uncomment for debugging. */
//         public String toString() {
//             return ("cursor=" + cursor + " " +
//                     "nextIndex=" + nextIndex + " " +
//                     "lastRet=" + lastRet + " " +
//                     "nextItem=" + nextItem + " " +
//                     "lastItem=" + lastItem + " " +
//                     "prevCycles=" + prevCycles + " " +
//                     "prevTakeIndex=" + prevTakeIndex + " " +
//                     "size()=" + size() + " " +
//                     "remainingCapacity()=" + remainingCapacity());
//         }
    }
    /**
     * Returns a {@link Spliterator} over the elements in this queue.
     *
     * <p>The returned spliterator is
     * <a href="package-summary.html#Weakly"><i>weakly consistent</i></a>.
     *
     * <p>The {@code Spliterator} reports {@link Spliterator#CONCURRENT},
     * {@link Spliterator#ORDERED}, and {@link Spliterator#NONNULL}.
     *
     * @implNote
     * The {@code Spliterator} implements {@code trySplit} to permit limited
     * parallelism.
     *
     * @return a {@code Spliterator} over the elements in this queue
     * @since 1.8
     */
    public Spliterator<E> spliterator() {
        return Spliterators.spliterator
            (this, Spliterator.ORDERED | Spliterator.NONNULL |
             Spliterator.CONCURRENT);
    }
    /**
     * Deserializes this queue and then checks some invariants.
     *
     * @param s the input stream
     * @throws ClassNotFoundException if the class of a serialized object
     *         could not be found
     * @throws java.io.InvalidObjectException if invariants are violated
     * @throws java.io.IOException if an I/O error occurs
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in items array and various fields
        s.defaultReadObject();
        // Check invariants over count and index fields. Note that
        // if putIndex==takeIndex, count can be either 0 or items.length.
        if (items.length == 0 ||
            takeIndex < 0 || takeIndex >= items.length ||
            putIndex  < 0 || putIndex  >= items.length ||
            count < 0     || count     >  items.length ||
            Math.floorMod(putIndex - takeIndex, items.length) !=
            Math.floorMod(count, items.length)) {
            throw new java.io.InvalidObjectException("invariants violated");
        }
    }
}

学习文章摘抄
原文地址:https://www.jianshu.com/p/9a652250e0d1

前言

本文的主要详细分析ArrayBlockingQueue的实现原理,由于该并发集合其底层是使用了java.util.ReentrantLock和java.util.Condition来完成并发控制的,我们可以通过JDK的源代码更好的学习这些并发控制类的使用,同时该类也是所有并发集合中最简单的一个,分析该类的源码也是为之后分析其他并发集合做好基础。

1.Queue接口和BlockingQueue接口回顾

1.1 Queue接口回顾

在Queue接口中,除了继承Collection接口中定义的方法外,它还分别额外地定义插入、删除、查询这3个操作,其中每一个操作都以两种不同的形式存在,每一种形式都对应着一个方法。
方法说明:
操作
抛出异常
返回特殊值
Insert
add(e)
offer(e)
Remove
remove()
poll()
Examine
element()
peek()
  1. add方法在将一个元素插入到队列的尾部时,如果出现队列已经满了,那么就会抛出IllegalStateException,而使用offer方法时,如果队列满了,则添加失败,返回false,但并不会引发异常。
  2. remove方法是获取队列的头部元素并且删除,如果当队列为空时,那么就会抛出NoSuchElementException。而poll在队列为空时,则返回一个null。
  3. element方法是从队列中获取到队列的第一个元素,但不会删除,但是如果队列为空时,那么它就会抛出NoSuchElementException。peek方法与之类似,只是不会抛出异常,而是返回false。
后面我们在分析ArrayBlockingQueue的方法时,主要也是围绕着这几个方法来进行分析。

1.2 BlockingQueue接口回顾

BlockingQueue是JDK1.5出现的接口,它在原来的Queue接口基础上提供了更多的额外功能:当获取队列中的头部元素时,如果队列为空,那么它将会使执行线程处于等待状态;当添加一个元素到队列的尾部时,如果队列已经满了,那么它同样会使执行的线程处于等待状态。
前面我们在说Queue接口时提到过,它针对于相同的操作提供了2种不同的形式,而BlockingQueue更夸张,针对于相同的操作提供了4种不同的形式。
该四种形式分别为:
对应的方法说明:
操作
抛出异常
返回特殊值
阻塞
超时
Insert
add(e)
offer(e)
put(e)
offer(e, time, unit)
Remove
remove()
poll()
take()
poll(time, unit)
Examine
element()
peek()
BlockingQueue虽然比起Queue在操作上提供了更多的支持,但是它在使用的使用也应该如下的几点:
  1. BlockingQueue中是不允许添加null的,该接受在声明的时候就要求所有的实现类在接收到一个null的时候,都应该抛出NullPointerException。
  1. BlockingQueue是线程安全的,因此它的所有和队列相关的方法都具有原子性。但是对于那么从Collection接口中继承而来的批量操作方法,比如addAll(Collection e)等方法,BlockingQueue的实现通常没有保证其具有原子性,因此我们在使用的BlockingQueue,应该尽可能地不去使用这些方法。
  2. BlockingQueue主要应用于生产者与消费者的模型中,其元素的添加和获取都是极具规律性的。但是对于remove(Object o)这样的方法,虽然BlockingQueue可以保证元素正确的删除,但是这样的操作会非常响应性能,因此我们在没有特殊的情况下,也应该避免使用这类方法。

2. ArrayBlockingQueue深入分析

有了上面的铺垫,下面我们就可以真正开始分析ArrayBlockingQueue了。在分析之前,首先让我们看看API对其的描述。
注意:这里使用的JDK版本为1.7,不同的JDK版本在实现上存在不同
ArrayBlockingQueueAPI说明.png
首先让我们看下ArrayBlockingQueue的核心组成:
/** 底层维护队列元素的数组 */
final Object[] items;

/** 当读取元素时数组的下标(这里称为读下标) */
int takeIndex;

/** 添加元素时数组的下标 (这里称为写小标)*/
int putIndex;

/** 队列中的元素个数 */
int count;

/**用于并发控制的工具类**/
final ReentrantLock lock;

/** 控制take操作时是否让线程等待 */
private final Condition notEmpty;

/** 控制put操作时是否让线程等待 */
private final Condition notFull;

take方法分析(369-379行):
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
/*
尝试获取锁,如果此时锁被其他线程锁占用,那么当前线程就处于Waiting的状态。
注意:当方法是支持线程中断响应的如果其他线程此时中断当前线程,
那么当前线程就会抛出InterruptedException
*/
lock.lockInterruptibly();
try {
/*
如果此时队列中的元素个数为0,那么就让当前线程wait,并且释放锁。
注意:这里使用了while进行重复检查,是为了防止当前线程可能由于 其他未知的原因被唤醒。
(通常这种情况被称为"spurious wakeup")
*/
while (count == 0)
notEmpty.await();
//如果队列不为空,则从队列的头部取元素
return extract();
} finally {
//完成锁的释放
lock.unlock();
}
}
extract方法分析(163-171):
/*
根据takeIndex来获取当前的元素,然后通知其他等待的线程。
Call only when holding lock.(只有当前线程已经持有了锁之后,它才能调用该方法)
*/
private E extract() {
final Object[] items = this.items;

//根据takeIndex获取元素,因为元素是一个Object类型的数组,因此它通过cast方法将其转换成泛型。
E x = this.<E>cast(items[takeIndex]);

//将当前位置的元素设置为null
items[takeIndex] = null;

//并且将takeIndex++,注意:这里因为已经使用了锁,因此inc方法中没有使用到原子操作
takeIndex = inc(takeIndex);
//将队列中的总的元素减1
--count;
//唤醒其他等待的线程
notFull.signal();
return x;
}
put方法分析(318-239)

public void put(E e) throws InterruptedException {
//首先检查元素是否为空,否则抛出NullPointerException
checkNotNull(e);
final ReentrantLock lock = this.lock;
//进行锁的抢占
lock.lockInterruptibly();
try {
/*当队列的长度等于数组的长度,此时说明队列已经满了,这里同样
使用了while来方式当前线程被"伪唤醒"。*/
while (count == items.length)
//则让当前线程处于等待状态
notFull.await();
//一旦获取到锁并且队列还未满时,则执行insert操作。
insert(e);
} finally {
//完成锁的释放
lock.unlock();
}
}

//检查元素是否为空
private static void checkNotNull(Object v) {
if (v == null)
throw new NullPointerException();
}

//该方法的逻辑非常简单
private void insert(E x) {
//将当前元素设置到putIndex位置
items[putIndex] = x;
//让putIndex++
putIndex = inc(putIndex);
//将队列的大小加1
++count;
//唤醒其他正在处于等待状态的线程
notEmpty.signal();
}
注:ArrayBlockingQueue其实是一个循环队列
我们使用一个图来简单说明一下:
黄色表示数组中有元素
1-1.png

当再一次执行put的时候,其结果为:
1-2.png
此时放入的元素会从头开始置,我们通过其incr方法更加清晰的看出其底层的操作:
/**
* Circularly increment i.
*/
final int inc(int i) {
//当takeIndex的值等于数组的长度时,就会重新置为0,这个一个循环递增的过程
return (++i == items.length) ? 0 : i;
}
至此,ArrayBlockingQueue的核心部分就分析完了,其余的队列操作基本上都是换汤不换药的,此处不再一一列举。


作者:码农一枚
链接:https://www.jianshu.com/p/9a652250e0d1
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


个人理解
1.LinkedBlockingQueue是一个带空头结点的链表,出队时,是将空节点的后一个节点的值置空,然后将后一个节点置为头结点,老头结点的next引用置为自己,从而帮助垃圾回收
2.因为最大的并发是一个写线程与一个读线程,所以不会出现任何的线程问题
3.双端队列就是依靠一个空头结点?
java8源码

/**
 * An optionally-bounded {@linkplain BlockingQueue blocking queue} based on
 * linked nodes.
 * This queue orders elements FIFO (first-in-first-out).
 * The <em>head</em> of the queue is that element that has been on the
 * queue the longest time.
 * The <em>tail</em> of the queue is that element that has been on the
 * queue the shortest time. New elements
 * are inserted at the tail of the queue, and the queue retrieval
 * operations obtain elements at the head of the queue.
 * Linked queues typically have higher throughput than array-based queues but
 * less predictable performance in most concurrent applications.
 *
 * <p>The optional capacity bound constructor argument serves as a
 * way to prevent excessive queue expansion. The capacity, if unspecified,
 * is equal to {@link Integer#MAX_VALUE}.  Linked nodes are
 * dynamically created upon each insertion unless this would bring the
 * queue above capacity.
 *
 * <p>This class and its iterator implement all of the
 * <em>optional</em> methods of the {@link Collection} and {@link
 * Iterator} interfaces.
 *
 * <p>This class is a member of the
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 * Java Collections Framework</a>.
 *
 * @since 1.5
 * @author Doug Lea
 * @param <E> the type of elements held in this collection
 */
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -6903933977591709194L;
    /*
     * A variant of the "two lock queue" algorithm.  The putLock gates
     * entry to put (and offer), and has an associated condition for
     * waiting puts.  Similarly for the takeLock.  The "count" field
     * that they both rely on is maintained as an atomic to avoid
     * needing to get both locks in most cases. Also, to minimize need
     * for puts to get takeLock and vice-versa, cascading notifies are
     * used. When a put notices that it has enabled at least one take,
     * it signals taker. That taker in turn signals others if more
     * items have been entered since the signal. And symmetrically for
     * takes signalling puts. Operations such as remove(Object) and
     * iterators acquire both locks.
     *
     * Visibility between writers and readers is provided as follows:
     *
     * Whenever an element is enqueued, the putLock is acquired and
     * count updated.  A subsequent reader guarantees visibility to the
     * enqueued Node by either acquiring the putLock (via fullyLock)
     * or by acquiring the takeLock, and then reading n = count.get();
     * this gives visibility to the first n items.
     *
     * To implement weakly consistent iterators, it appears we need to
     * keep all Nodes GC-reachable from a predecessor dequeued Node.
     * That would cause two problems:
     * - allow a rogue Iterator to cause unbounded memory retention
     * - cause cross-generational linking of old Nodes to new Nodes if
     *   a Node was tenured while live, which generational GCs have a
     *   hard time dealing with, causing repeated major collections.
     * However, only non-deleted Nodes need to be reachable from
     * dequeued Nodes, and reachability does not necessarily have to
     * be of the kind understood by the GC.  We use the trick of
     * linking a Node that has just been dequeued to itself.  Such a
     * self-link implicitly means to advance to head.next.
     */
    /**
     * Linked list node class
     */
    static class Node<E> {
        E item;
        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head.next
         * - null, meaning there is no successor (this is the last node)
         */
        Node<E> next;
        Node(E x) { item = x; }
    }
    /** The capacity bound, or Integer.MAX_VALUE if none */
    private final int capacity;
    /** Current number of elements */
    private final AtomicInteger count = new AtomicInteger();
    /**
     * Head of linked list.
     * Invariant: head.item == null
     */
    transient Node<E> head;
    /**
     * Tail of linked list.
     * Invariant: last.next == null
     */
    private transient Node<E> last;
    /** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();
    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();
    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();
    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();
    /**
     * Signals a waiting take. Called only from put/offer (which do not
     * otherwise ordinarily lock takeLock.)
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }
    /**
     * Signals a waiting put. Called only from take/poll.
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }
    /**
     * Links node at end of queue.
     *
     * @param node the node
     */
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }
    /**
     * Removes a node from head of queue.
     *
     * @return the node
     */
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }
    /**
     * Locks to prevent both puts and takes.
     */
    void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }
    /**
     * Unlocks to allow both puts and takes.
     */
    void fullyUnlock() {
        takeLock.unlock();
        putLock.unlock();
    }
//     /**
//      * Tells whether both locks are held by current thread.
//      */
//     boolean isFullyLocked() {
//         return (putLock.isHeldByCurrentThread() &&
//                 takeLock.isHeldByCurrentThread());
//     }
    /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
    /**
     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater
     *         than zero
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }
    /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}, initially containing the elements of the
     * given collection,
     * added in traversal order of the collection's iterator.
     *
     * @param c the collection of elements to initially contain
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }
    // this doc comment is overridden to remove the reference to collections
    // greater in size than Integer.MAX_VALUE
    /**
     * Returns the number of elements in this queue.
     *
     * @return the number of elements in this queue
     */
    public int size() {
        return count.get();
    }
    // this doc comment is a modified copy of the inherited doc comment,
    // without the reference to unlimited queues.
    /**
     * Returns the number of additional elements that this queue can ideally
     * (in the absence of memory or resource constraints) accept without
     * blocking. This is always equal to the initial capacity of this queue
     * less the current {@code size} of this queue.
     *
     * <p>Note that you <em>cannot</em> always tell if an attempt to insert
     * an element will succeed by inspecting {@code remainingCapacity}
     * because it may be the case that another thread is about to
     * insert or remove an element.
     */
    public int remainingCapacity() {
        return capacity - count.get();
    }
    /**
     * Inserts the specified element at the tail of this queue, waiting if
     * necessary for space to become available.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }
    /**
     * Inserts the specified element at the tail of this queue, waiting if
     * necessary up to the specified wait time for space to become available.
     *
     * @return {@code true} if successful, or {@code false} if
     *         the specified waiting time elapses before space is available
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
        if (e == null) throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }
    /**
     * Inserts the specified element at the tail of this queue if it is
     * possible to do so immediately without exceeding the queue's capacity,
     * returning {@code true} upon success and {@code false} if this queue
     * is full.
     * When using a capacity-restricted queue, this method is generally
     * preferable to method {@link BlockingQueue#add add}, which can fail to
     * insert an element only by throwing an exception.
     *
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity)
            return false;
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            if (count.get() < capacity) {
                enqueue(node);
                c = count.getAndIncrement();
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }
    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E x = null;
        int c = -1;
        long nanos = unit.toNanos(timeout);
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
    public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            if (count.get() > 0) {
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
    public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }
    /**
     * Unlinks interior Node p with predecessor trail.
     */
    void unlink(Node<E> p, Node<E> trail) {
        // assert isFullyLocked();
        // p.next is not changed, to allow iterators that are
        // traversing p to maintain their weak-consistency guarantee.
        p.item = null;
        trail.next = p.next;
        if (last == p)
            last = trail;
        if (count.getAndDecrement() == capacity)
            notFull.signal();
    }
    /**
     * Removes a single instance of the specified element from this queue,
     * if it is present.  More formally, removes an element {@code e} such
     * that {@code o.equals(e)}, if this queue contains one or more such
     * elements.
     * Returns {@code true} if this queue contained the specified element
     * (or equivalently, if this queue changed as a result of the call).
     *
     * @param o element to be removed from this queue, if present
     * @return {@code true} if this queue changed as a result of the call
     */
    public boolean remove(Object o) {
        if (o == null) return false;
        fullyLock();
        try {
            for (Node<E> trail = head, p = trail.next;
                 p != null;
                 trail = p, p = p.next) {
                if (o.equals(p.item)) {
                    unlink(p, trail);
                    return true;
                }
            }
            return false;
        } finally {
            fullyUnlock();
        }
    }
    /**
     * Returns {@code true} if this queue contains the specified element.
     * More formally, returns {@code true} if and only if this queue contains
     * at least one element {@code e} such that {@code o.equals(e)}.
     *
     * @param o object to be checked for containment in this queue
     * @return {@code true} if this queue contains the specified element
     */
    public boolean contains(Object o) {
        if (o == null) return false;
        fullyLock();
        try {
            for (Node<E> p = head.next; p != null; p = p.next)
                if (o.equals(p.item))
                    return true;
            return false;
        } finally {
            fullyUnlock();
        }
    }
    /**
     * Returns an array containing all of the elements in this queue, in
     * proper sequence.
     *
     * <p>The returned array will be "safe" in that no references to it are
     * maintained by this queue.  (In other words, this method must allocate
     * a new array).  The caller is thus free to modify the returned array.
     *
     * <p>This method acts as bridge between array-based and collection-based
     * APIs.
     *
     * @return an array containing all of the elements in this queue
     */
    public Object[] toArray() {
        fullyLock();
        try {
            int size = count.get();
            Object[] a = new Object[size];
            int k = 0;
            for (Node<E> p = head.next; p != null; p = p.next)
                a[k++] = p.item;
            return a;
        } finally {
            fullyUnlock();
        }
    }
    /**
     * Returns an array containing all of the elements in this queue, in
     * proper sequence; the runtime type of the returned array is that of
     * the specified array.  If the queue fits in the specified array, it
     * is returned therein.  Otherwise, a new array is allocated with the
     * runtime type of the specified array and the size of this queue.
     *
     * <p>If this queue fits in the specified array with room to spare
     * (i.e., the array has more elements than this queue), the element in
     * the array immediately following the end of the queue is set to
     * {@code null}.
     *
     * <p>Like the {@link #toArray()} method, this method acts as bridge between
     * array-based and collection-based APIs.  Further, this method allows
     * precise control over the runtime type of the output array, and may,
     * under certain circumstances, be used to save allocation costs.
     *
     * <p>Suppose {@code x} is a queue known to contain only strings.
     * The following code can be used to dump the queue into a newly
     * allocated array of {@code String}:
     *
     *  <pre> {@code String[] y = x.toArray(new String[0]);}</pre>
     *
     * Note that {@code toArray(new Object[0])} is identical in function to
     * {@code toArray()}.
     *
     * @param a the array into which the elements of the queue are to
     *          be stored, if it is big enough; otherwise, a new array of the
     *          same runtime type is allocated for this purpose
     * @return an array containing all of the elements in this queue
     * @throws ArrayStoreException if the runtime type of the specified array
     *         is not a supertype of the runtime type of every element in
     *         this queue
     * @throws NullPointerException if the specified array is null
     */
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        fullyLock();
        try {
            int size = count.get();
            if (a.length < size)
                a = (T[])java.lang.reflect.Array.newInstance
                    (a.getClass().getComponentType(), size);
            int k = 0;
            for (Node<E> p = head.next; p != null; p = p.next)
                a[k++] = (T)p.item;
            if (a.length > k)
                a[k] = null;
            return a;
        } finally {
            fullyUnlock();
        }
    }
    public String toString() {
        fullyLock();
        try {
            Node<E> p = head.next;
            if (p == null)
                return "[]";
            StringBuilder sb = new StringBuilder();
            sb.append('[');
            for (;;) {
                E e = p.item;
                sb.append(e == this ? "(this Collection)" : e);
                p = p.next;
                if (p == null)
                    return sb.append(']').toString();
                sb.append(',').append(' ');
            }
        } finally {
            fullyUnlock();
        }
    }
    /**
     * Atomically removes all of the elements from this queue.
     * The queue will be empty after this call returns.
     */
    public void clear() {
        fullyLock();
        try {
            for (Node<E> p, h = head; (p = h.next) != null; h = p) {
                h.next = h;
                p.item = null;
            }
            head = last;
            // assert head.item == null && head.next == null;
            if (count.getAndSet(0) == capacity)
                notFull.signal();
        } finally {
            fullyUnlock();
        }
    }
    /**
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     * @throws IllegalArgumentException      {@inheritDoc}
     */
    public int drainTo(Collection<? super E> c) {
        return drainTo(c, Integer.MAX_VALUE);
    }
    /**
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     * @throws IllegalArgumentException      {@inheritDoc}
     */
    public int drainTo(Collection<? super E> c, int maxElements) {
        if (c == null)
            throw new NullPointerException();
        if (c == this)
            throw new IllegalArgumentException();
        if (maxElements <= 0)
            return 0;
        boolean signalNotFull = false;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            int n = Math.min(maxElements, count.get());
            // count.get provides visibility to first n Nodes
            Node<E> h = head;
            int i = 0;
            try {
                while (i < n) {
                    Node<E> p = h.next;
                    c.add(p.item);
                    p.item = null;
                    h.next = h;
                    h = p;
                    ++i;
                }
                return n;
            } finally {
                // Restore invariants even if c.add() threw
                if (i > 0) {
                    // assert h.item == null;
                    head = h;
                    signalNotFull = (count.getAndAdd(-i) == capacity);
                }
            }
        } finally {
            takeLock.unlock();
            if (signalNotFull)
                signalNotFull();
        }
    }
    /**
     * Returns an iterator over the elements in this queue in proper sequence.
     * The elements will be returned in order from first (head) to last (tail).
     *
     * <p>The returned iterator is
     * <a href="package-summary.html#Weakly"><i>weakly consistent</i></a>.
     *
     * @return an iterator over the elements in this queue in proper sequence
     */
    public Iterator<E> iterator() {
        return new Itr();
    }
    private class Itr implements Iterator<E> {
        /*
         * Basic weakly-consistent iterator.  At all times hold the next
         * item to hand out so that if hasNext() reports true, we will
         * still have it to return even if lost race with a take etc.
         */
        private Node<E> current;
        private Node<E> lastRet;
        private E currentElement;
        Itr() {
            fullyLock();
            try {
                current = head.next;
                if (current != null)
                    currentElement = current.item;
            } finally {
                fullyUnlock();
            }
        }
        public boolean hasNext() {
            return current != null;
        }
        /**
         * Returns the next live successor of p, or null if no such.
         *
         * Unlike other traversal methods, iterators need to handle both:
         * - dequeued nodes (p.next == p)
         * - (possibly multiple) interior removed nodes (p.item == null)
         */
        private Node<E> nextNode(Node<E> p) {
            for (;;) {
                Node<E> s = p.next;
                if (s == p)
                    return head.next;
                if (s == null || s.item != null)
                    return s;
                p = s;
            }
        }
        public E next() {
            fullyLock();
            try {
                if (current == null)
                    throw new NoSuchElementException();
                E x = currentElement;
                lastRet = current;
                current = nextNode(current);
                currentElement = (current == null) ? null : current.item;
                return x;
            } finally {
                fullyUnlock();
            }
        }
        public void remove() {
            if (lastRet == null)
                throw new IllegalStateException();
            fullyLock();
            try {
                Node<E> node = lastRet;
                lastRet = null;
                for (Node<E> trail = head, p = trail.next;
                     p != null;
                     trail = p, p = p.next) {
                    if (p == node) {
                        unlink(p, trail);
                        break;
                    }
                }
            } finally {
                fullyUnlock();
            }
        }
    }
    /** A customized variant of Spliterators.IteratorSpliterator */
    static final class LBQSpliterator<E> implements Spliterator<E> {
        static final int MAX_BATCH = 1 << 25;  // max batch array size;
        final LinkedBlockingQueue<E> queue;
        Node<E> current;    // current node; null until initialized
        int batch;          // batch size for splits
        boolean exhausted // true when no more nodes
        long est;           // size estimate
        LBQSpliterator(LinkedBlockingQueue<E> queue) {
            this.queue = queue;
            this.est = queue.size();
        }
        public long estimateSize() { return est; }
        public Spliterator<E> trySplit() {
            Node<E> h;
            final LinkedBlockingQueue<E> q = this.queue;
            int b = batch;
            int n = (b <= 0) ? 1 : (b >= MAX_BATCH) ? MAX_BATCH : b + 1;
            if (!exhausted &&
                ((h = current) != null || (h = q.head.next) != null) &&
                h.next != null) {
                Object[] a = new Object[n];
                int i = 0;
                Node<E> p = current;
                q.fullyLock();
                try {
                    if (p != null || (p = q.head.next) != null) {
                        do {
                            if ((a[i] = p.item) != null)
                                ++i;
                        } while ((p = p.next) != null && i < n);
                    }
                } finally {
                    q.fullyUnlock();
                }
                if ((current = p) == null) {
                    est = 0L;
                    exhausted = true;
                }
                else if ((est -= i) < 0L)
                    est = 0L;
                if (i > 0) {
                    batch = i;
                    return Spliterators.spliterator
                        (a, 0, i, Spliterator.ORDERED | Spliterator.NONNULL |
                         Spliterator.CONCURRENT);
                }
            }
            return null;
        }
        public void forEachRemaining(Consumer<? super E> action) {
            if (action == null) throw new NullPointerException();
            final LinkedBlockingQueue<E> q = this.queue;
            if (!exhausted) {
                exhausted = true;
                Node<E> p = current;
                do {
                    E e = null;
                    q.fullyLock();
                    try {
                        if (p == null)
                            p = q.head.next;
                        while (p != null) {
                            e = p.item;
                            p = p.next;
                            if (e != null)
                                break;
                        }
                    } finally {
                        q.fullyUnlock();
                    }
                    if (e != null)
                        action.accept(e);
                } while (p != null);
            }
        }
        public boolean tryAdvance(Consumer<? super E> action) {
            if (action == null) throw new NullPointerException();
            final LinkedBlockingQueue<E> q = this.queue;
            if (!exhausted) {
                E e = null;
                q.fullyLock();
                try {
                    if (current == null)
                        current = q.head.next;
                    while (current != null) {
                        e = current.item;
                        current = current.next;
                        if (e != null)
                            break;
                    }
                } finally {
                    q.fullyUnlock();
                }
                if (current == null)
                    exhausted = true;
                if (e != null) {
                    action.accept(e);
                    return true;
                }
            }
            return false;
        }
        public int characteristics() {
            return Spliterator.ORDERED | Spliterator.NONNULL |
                Spliterator.CONCURRENT;
        }
    }
    /**
     * Returns a {@link Spliterator} over the elements in this queue.
     *
     * <p>The returned spliterator is
     * <a href="package-summary.html#Weakly"><i>weakly consistent</i></a>.
     *
     * <p>The {@code Spliterator} reports {@link Spliterator#CONCURRENT},
     * {@link Spliterator#ORDERED}, and {@link Spliterator#NONNULL}.
     *
     * @implNote
     * The {@code Spliterator} implements {@code trySplit} to permit limited
     * parallelism.
     *
     * @return a {@code Spliterator} over the elements in this queue
     * @since 1.8
     */
    public Spliterator<E> spliterator() {
        return new LBQSpliterator<E>(this);
    }
    /**
     * Saves this queue to a stream (that is, serializes it).
     *
     * @param s the stream
     * @throws java.io.IOException if an I/O error occurs
     * @serialData The capacity is emitted (int), followed by all of
     * its elements (each an {@code Object}) in the proper order,
     * followed by a null
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        fullyLock();
        try {
            // Write out any hidden stuff, plus capacity
            s.defaultWriteObject();
            // Write out all elements in the proper order.
            for (Node<E> p = head.next; p != null; p = p.next)
                s.writeObject(p.item);
            // Use trailing null as sentinel
            s.writeObject(null);
        } finally {
            fullyUnlock();
        }
    }
    /**
     * Reconstitutes this queue from a stream (that is, deserializes it).
     * @param s the stream
     * @throws ClassNotFoundException if the class of a serialized object
     *         could not be found
     * @throws java.io.IOException if an I/O error occurs
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in capacity, and any hidden stuff
        s.defaultReadObject();
        count.set(0);
        last = head = new Node<E>(null);
        // Read in all elements and place in queue
        for (;;) {
            @SuppressWarnings("unchecked")
            E item = (E)s.readObject();
            if (item == null)
                break;
            add(item);
        }
    }
}

原文
地址:https://www.jianshu.com/p/cc2281b1a6bc

前言

在前面的文章ArrayBlockingQueue源码分析中,已经对JDK中的BlockingQueue中的做了一个回顾,同时对ArrayBlockingQueue中的核心方法作了说明,而LinkedBlockingQueue作为JDK中BlockingQueue家族系列中一员,由于其作为固定大小线程池(Executors.newFixedThreadPool())底层所使用的阻塞队列,分析它的目的主要在于2点:
(1) 与ArrayBlockingQueue进行类比学习,加深各种数据结构的理解
(2) 了解底层实现,能够更好地理解每一种阻塞队列对线程池性能的影响,做到真正的知其然,且知其所以然

1.LinkedBlockingQueue深入分析

LinkedBlockingQueue,见名之意,它是由一个基于链表的阻塞队列,首先看一下的核心组成:
// 所有的元素都通过Node这个静态内部类来进行存储,这与LinkedList的处理方式完全一样
static class Node<E> {
//使用item来保存元素本身
E item;
//保存当前节点的后继节点
Node<E> next;
Node(E x) { item = x; }
}
/**
阻塞队列所能存储的最大容量
用户可以在创建时手动指定最大容量,如果用户没有指定最大容量
那么最默认的最大容量为Integer.MAX_VALUE.
*/
private final int capacity;

/**
当前阻塞队列中的元素数量
PS:如果你看过ArrayBlockingQueue的源码,你会发现
ArrayBlockingQueue底层保存元素数量使用的是一个
普通的int类型变量。其原因是在ArrayBlockingQueue底层
对于元素的入队列和出队列使用的是同一个lock对象。而数
量的修改都是在处于线程获取锁的情况下进行操作,因此不
会有线程安全问题。
而LinkedBlockingQueue却不是,它的入队列和出队列使用的是两个
不同的lock对象,因此无论是在入队列还是出队列,都会涉及对元素数
量的并发修改,(之后通过源码可以更加清楚地看到)因此这里使用了一个原子操作类
来解决对同一个变量进行并发修改的线程安全问题。
*/
private final AtomicInteger count = new AtomicInteger(0);

/**
* 链表的头部
* LinkedBlockingQueue的头部具有一个不变性:
* 头部的元素总是为null,head.item==null
*/
private transient Node<E> head;

/**
* 链表的尾部
* LinkedBlockingQueue的尾部也具有一个不变性:
* 即last.next==null
*/
private transient Node<E> last;

/**
元素出队列时线程所获取的锁
当执行take、poll等操作时线程需要获取的锁
*/
private final ReentrantLock takeLock = new ReentrantLock();

/**
当队列为空时,通过该Condition让从队列中获取元素的线程处于等待状态
*/
private final Condition notEmpty = takeLock.newCondition();

/**
元素入队列时线程所获取的锁
当执行add、put、offer等操作时线程需要获取锁
*/
private final ReentrantLock putLock = new ReentrantLock();

/**
当队列的元素已经达到capactiy,通过该Condition让元素入队列的线程处于等待状态
*/
private final Condition notFull = putLock.newCondition();

通过上面的分析,我们可以发现LinkedBlockingQueue在入队列和出队列时使用的不是同一个Lock,这也意味着它们之间的操作不会存在互斥操作。在多个CPU的情况下,它们可以做到真正的在同一时刻既消费、又生产,能够做到并行处理。
下面让我们看下LinkedBlockingQueue的构造方法:
/**
* 如果用户没有显示指定capacity的值,默认使用int的最大值
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
可以看到,当队列中没有任何元素的时候,此时队列的头部就等于队列的尾部,
指向的是同一个节点,并且元素的内容为null
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}

/*
在初始化LinkedBlockingQueue的时候,还可以直接将一个集合
中的元素全部入队列,此时队列最大容量依然是int的最大值。
*/
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
//获取锁
putLock.lock(); // Never contended, but necessary for visibility
try {
//迭代集合中的每一个元素,让其入队列,并且更新一下当前队列中的元素数量
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
//参考下面的enqueue分析
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
//释放锁
putLock.unlock();
}
}

/**
* 我去,这代码其实可读性不怎么样啊。
* 其实下面的代码等价于如下内容:
* last.next=node;
* last = node;
* 其实也没有什么花样:
就是让新入队列的元素成为原来的last的next,让进入的元素称为last
*
*/
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}

在分析完LinkedBlockingQueue的核心组成之后,下面让我们再看下核心的几个操作方法,首先分析一下元素入队列的过程:
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
/*注意上面这句话,约定所有的put/take操作都会预先设置本地变量,
可以看到下面有一个将putLock赋值给了一个局部变量的操作
*/
int c = -1;
Node<E> node = new Node(e);
/*
在这里首先获取到putLock,以及当前队列的元素数量
即上面所描述的预设置本地变量操作
*/
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
/*
执行可中断的锁获取操作,即意味着如果线程由于获取
锁而处于Blocked状态时,线程是可以被中断而不再继
续等待,这也是一种避免死锁的一种方式,不会因为
发现到死锁之后而由于无法中断线程最终只能重启应用。
*/
putLock.lockInterruptibly();
try {
/*
当队列的容量到底最大容量时,此时线程将处于等待状
态,直到队列有空闲的位置才继续执行。使用while判
断依旧是为了放置线程被"伪唤醒”而出现的情况,即当
线程被唤醒时而队列的大小依旧等于capacity时,线
程应该继续等待。
*/
while (count.get() == capacity) {
notFull.await();
}
//让元素进行队列的末尾,enqueue代码在上面分析过了
enqueue(node);
//首先获取原先队列中的元素个数,然后再对队列中的元素个数+1.
c = count.getAndIncrement();
/*注:c+1得到的结果是新元素入队列之后队列元素的总和。
当前队列中的总元素个数小于最大容量时,此时唤醒其他执行入队列的线程
让它们可以放入元素,如果新加入元素之后,队列的大小等于capacity,
那么就意味着此时队列已经满了,也就没有必须要唤醒其他正在等待入队列的线程,因为唤醒它们之后,它们也还是继续等待。
*/
if (c + 1 < capacity)
notFull.signal();
} finally {
//完成对锁的释放
putLock.unlock();
}
/*当c=0时,即意味着之前的队列是空队列,出队列的线程都处于等待状态,
现在新添加了一个新的元素,即队列不再为空,因此它会唤醒正在等待获取元素的线程。
*/
if (c == 0)
signalNotEmpty();
}
/*
唤醒正在等待获取元素的线程,告诉它们现在队列中有元素了
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
//通过notEmpty唤醒获取元素的线程
notEmpty.signal();
} finally {
takeLock.unlock();
}
}

看完put方法,下面再看看下offer是如何处理的方法:
/**
在BlockingQueue接口中除了定义put方法外(当队列元素满了之后就会阻塞,
直到队列有新的空间可以方法线程才会继续执行),还定义一个offer方法,
该方法会返回一个boolean值,当入队列成功返回true,入队列失败返回false。
该方法与put方法基本操作基本一致,只是有细微的差异。
*/
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
/*
当队列已经满了,它不会继续等待,而是直接返回。
因此该方法是非阻塞的。
*/
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
/*
当获取到锁时,需要进行二次的检查,因为可能当队列的大小为capacity-1时,
两个线程同时去抢占锁,而只有一个线程抢占成功,那么此时
当线程将元素入队列后,释放锁,后面的线程抢占锁之后,此时队列
大小已经达到capacity,所以将它无法让元素入队列。
下面的其余操作和put都一样,此处不再详述
*/
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
BlockingQueue还定义了一个限时等待插入操作,即在等待一定的时间内,如果队列有空间可以插入,那么就将元素入队列,然后返回true,如果在过完指定的时间后依旧没有空间可以插入,那么就返回false,下面是限时等待操作的分析:
/**
通过timeout和TimeUnit来指定等待的时长
timeout为时间的长度,TimeUnit为时间的单位
*/
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {

if (e == null) throw new NullPointerException();
//将指定的时间长度转换为毫秒来进行处理
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
//如果等待的剩余时间小于等于0,那么直接返回
if (nanos <= 0)
return false;
/*
通过condition来完成等待,此时当前线程会完成锁的,并且处于等待状态
直到被其他线程唤醒该线程、或者当前线程被中断、
等待的时间截至才会返回,该返回值为从方法调用到返回所经历的时长。
注意:上面的代码是condition的awitNanos()方法的通用写法,
可以参看Condition.awaitNaos的API文档。
下面的其余操作和put都一样,此处不再详述
*/
nanos = notFull.awaitNanos(nanos);
}
enqueue(new Node<E>(e));
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
通过上面的分析,我们应该比较清楚地知道了LinkedBlockingQueue的入队列的操作,其主要是通过获取到putLock锁来完成,当队列的数量达到最大值,此时会导致线程处于阻塞状态或者返回false(根据具体的方法来看);如果队列还有剩余的空间,那么此时会新创建出一个Node对象,将其设置到队列的尾部,作为LinkedBlockingQueue的last元素。
在分析完入队列的过程之后,我们接下来看看LinkedBlockingQueue出队列的过程;由于BlockingQueue的方法都具有对称性,此处就只分析take方法的实现,其余方法的实现都如出一辙:

public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//通过takeLock获取锁,并且支持线程中断
takeLock.lockInterruptibly();
try {
//当队列为空时,则让当前线程处于等待
while (count.get() == 0) {
notEmpty.await();
}
//完成元素的出队列
x = dequeue();
/*
队列元素个数完成原子化操作-1,可以看到count元素会
在插入元素的线程和获取元素的线程进行并发修改操作。
*/
c = count.getAndDecrement();
/*
当一个元素出队列之后,队列的大小依旧大于1时
当前线程会唤醒其他执行元素出队列的线程,让它们也
可以执行元素的获取
*/
if (c > 1)
notEmpty.signal();
} finally {
//完成锁的释放
takeLock.unlock();
}
/*
当c==capaitcy时,即在获取当前元素之前,
队列已经满了,而此时获取元素之后,队列就会
空出一个位置,故当前线程会唤醒执行插入操作的线
程通知其他中的一个可以进行插入操作。
*/
if (c == capacity)
signalNotFull();
return x;
}


/**
* 让头部元素出队列的过程
* 其最终的目的是让原来的head被GC回收,让其的next成为head
* 并且新的head的item为null.
* 因为LinkedBlockingQueue的头部具有一致性:即元素为null。
*/
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
LinkedBlockingQueue出队列大致过程.png
对于LinkedBlockingQueue的源码分析就到这里,下面让我们将LinkedBlockingQueue与ArrayBlockingQueue进行一个比较。

2.LinkedBlockingQueue与ArrayBlockingQueue的比较

ArrayBlockingQueue由于其底层基于数组,并且在创建时指定存储的大小,在完成后就会立即在内存分配固定大小容量的数组元素,因此其存储通常有限,故其是一个“有界“的阻塞队列;而LinkedBlockingQueue可以由用户指定最大存储容量,也可以无需指定,如果不指定则最大存储容量将是Integer.MAX_VALUE,即可以看作是一个“无界”的阻塞队列,由于其节点的创建都是动态创建,并且在节点出队列后可以被GC所回收,因此其具有灵活的伸缩性。但是由于ArrayBlockingQueue的有界性,因此其能够更好的对于性能进行预测,而LinkedBlockingQueue由于没有限制大小,当任务非常多的时候,不停地向队列中存储,就有可能导致内存溢出的情况发生。
其次,ArrayBlockingQueue中在入队列和出队列操作过程中,使用的是同一个lock,所以即使在多核CPU的情况下,其读取和操作的都无法做到并行,而LinkedBlockingQueue的读取和插入操作所使用的锁是两个不同的lock,它们之间的操作互相不受干扰,因此两种操作可以并行完成,故LinkedBlockingQueue的吞吐量要高于ArrayBlockingQueue。

3.选择LinkedBlockingQueue的理由

/**
下面的代码是Executors创建固定大小线程池的代码,其使用了
LinkedBlockingQueue来作为任务队列。
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
JDK中选用LinkedBlockingQueue作为阻塞队列的原因就在于其无界性。因为线程大小固定的线程池,其线程的数量是不具备伸缩性的,当任务非常繁忙的时候,就势必会导致所有的线程都处于工作状态,如果使用一个有界的阻塞队列来进行处理,那么就非常有可能很快导致队列满的情况发生,从而导致任务无法提交而抛出RejectedExecutionException,而使用无界队列由于其良好的存储容量的伸缩性,可以很好的去缓冲任务繁忙情况下场景,即使任务非常多,也可以进行动态扩容,当任务被处理完成之后,队列中的节点也会被随之被GC回收,非常灵活。
至此,LinkedBlockingQueue的分析就到这里,如果您发现有任何编写不对的地方,请指出(万分感谢!)。


作者:码农一枚
链接:https://www.jianshu.com/p/cc2281b1a6bc
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


个人理解

java8源码
/**
 * An optionally-bounded {@linkplain BlockingQueue blocking queue} based on
 * linked nodes.
 * This queue orders elements FIFO (first-in-first-out).
 * The <em>head</em> of the queue is that element that has been on the
 * queue the longest time.
 * The <em>tail</em> of the queue is that element that has been on the
 * queue the shortest time. New elements
 * are inserted at the tail of the queue, and the queue retrieval
 * operations obtain elements at the head of the queue.
 * Linked queues typically have higher throughput than array-based queues but
 * less predictable performance in most concurrent applications.
 *
 * <p>The optional capacity bound constructor argument serves as a
 * way to prevent excessive queue expansion. The capacity, if unspecified,
 * is equal to {@link Integer#MAX_VALUE}.  Linked nodes are
 * dynamically created upon each insertion unless this would bring the
 * queue above capacity.
 *
 * <p>This class and its iterator implement all of the
 * <em>optional</em> methods of the {@link Collection} and {@link
 * Iterator} interfaces.
 *
 * <p>This class is a member of the
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 * Java Collections Framework</a>.
 *
 * @since 1.5
 * @author Doug Lea
 * @param <E> the type of elements held in this collection
 */
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -6903933977591709194L;
    /*
     * A variant of the "two lock queue" algorithm.  The putLock gates
     * entry to put (and offer), and has an associated condition for
     * waiting puts.  Similarly for the takeLock.  The "count" field
     * that they both rely on is maintained as an atomic to avoid
     * needing to get both locks in most cases. Also, to minimize need
     * for puts to get takeLock and vice-versa, cascading notifies are
     * used. When a put notices that it has enabled at least one take,
     * it signals taker. That taker in turn signals others if more
     * items have been entered since the signal. And symmetrically for
     * takes signalling puts. Operations such as remove(Object) and
     * iterators acquire both locks.
     *
     * Visibility between writers and readers is provided as follows:
     *
     * Whenever an element is enqueued, the putLock is acquired and
     * count updated.  A subsequent reader guarantees visibility to the
     * enqueued Node by either acquiring the putLock (via fullyLock)
     * or by acquiring the takeLock, and then reading n = count.get();
     * this gives visibility to the first n items.
     *
     * To implement weakly consistent iterators, it appears we need to
     * keep all Nodes GC-reachable from a predecessor dequeued Node.
     * That would cause two problems:
     * - allow a rogue Iterator to cause unbounded memory retention
     * - cause cross-generational linking of old Nodes to new Nodes if
     *   a Node was tenured while live, which generational GCs have a
     *   hard time dealing with, causing repeated major collections.
     * However, only non-deleted Nodes need to be reachable from
     * dequeued Nodes, and reachability does not necessarily have to
     * be of the kind understood by the GC.  We use the trick of
     * linking a Node that has just been dequeued to itself.  Such a
     * self-link implicitly means to advance to head.next.
     */
    /**
     * Linked list node class
     */
    static class Node<E> {
        E item;
        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head.next
         * - null, meaning there is no successor (this is the last node)
         */
        Node<E> next;
        Node(E x) { item = x; }
    }
    /** The capacity bound, or Integer.MAX_VALUE if none */
    private final int capacity;
    /** Current number of elements */
    private final AtomicInteger count = new AtomicInteger();
    /**
     * Head of linked list.
     * Invariant: head.item == null
     */
    transient Node<E> head;
    /**
     * Tail of linked list.
     * Invariant: last.next == null
     */
    private transient Node<E> last;
    /** Lock held by take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();
    /** Wait queue for waiting takes */
    private final Condition notEmpty = takeLock.newCondition();
    /** Lock held by put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();
    /** Wait queue for waiting puts */
    private final Condition notFull = putLock.newCondition();
    /**
     * Signals a waiting take. Called only from put/offer (which do not
     * otherwise ordinarily lock takeLock.)
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }
    /**
     * Signals a waiting put. Called only from take/poll.
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }
    /**
     * Links node at end of queue.
     *
     * @param node the node
     */
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }
    /**
     * Removes a node from head of queue.
     *
     * @return the node
     */
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }
    /**
     * Locks to prevent both puts and takes.
     */
    void fullyLock() {
        putLock.lock();
        takeLock.lock();
    }
    /**
     * Unlocks to allow both puts and takes.
     */
    void fullyUnlock() {
        takeLock.unlock();
        putLock.unlock();
    }
//     /**
//      * Tells whether both locks are held by current thread.
//      */
//     boolean isFullyLocked() {
//         return (putLock.isHeldByCurrentThread() &&
//                 takeLock.isHeldByCurrentThread());
//     }
    /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
    /**
     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater
     *         than zero
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }
    /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}, initially containing the elements of the
     * given collection,
     * added in traversal order of the collection's iterator.
     *
     * @param c the collection of elements to initially contain
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }
    // this doc comment is overridden to remove the reference to collections
    // greater in size than Integer.MAX_VALUE
    /**
     * Returns the number of elements in this queue.
     *
     * @return the number of elements in this queue
     */
    public int size() {
        return count.get();
    }
    // this doc comment is a modified copy of the inherited doc comment,
    // without the reference to unlimited queues.
    /**
     * Returns the number of additional elements that this queue can ideally
     * (in the absence of memory or resource constraints) accept without
     * blocking. This is always equal to the initial capacity of this queue
     * less the current {@code size} of this queue.
     *
     * <p>Note that you <em>cannot</em> always tell if an attempt to insert
     * an element will succeed by inspecting {@code remainingCapacity}
     * because it may be the case that another thread is about to
     * insert or remove an element.
     */
    public int remainingCapacity() {
        return capacity - count.get();
    }
    /**
     * Inserts the specified element at the tail of this queue, waiting if
     * necessary for space to become available.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }
    /**
     * Inserts the specified element at the tail of this queue, waiting if
     * necessary up to the specified wait time for space to become available.
     *
     * @return {@code true} if successful, or {@code false} if
     *         the specified waiting time elapses before space is available
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
        if (e == null) throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }
    /**
     * Inserts the specified element at the tail of this queue if it is
     * possible to do so immediately without exceeding the queue's capacity,
     * returning {@code true} upon success and {@code false} if this queue
     * is full.
     * When using a capacity-restricted queue, this method is generally
     * preferable to method {@link BlockingQueue#add add}, which can fail to
     * insert an element only by throwing an exception.
     *
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity)
            return false;
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            if (count.get() < capacity) {
                enqueue(node);
                c = count.getAndIncrement();
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }
    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E x = null;
        int c = -1;
        long nanos = unit.toNanos(timeout);
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
    public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            if (count.get() > 0) {
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
    public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }
    /**
     * Unlinks interior Node p with predecessor trail.
     */
    void unlink(Node<E> p, Node<E> trail) {
        // assert isFullyLocked();
        // p.next is not changed, to allow iterators that are
        // traversing p to maintain their weak-consistency guarantee.
        p.item = null;
        trail.next = p.next;
        if (last == p)
            last = trail;
        if (count.getAndDecrement() == capacity)
            notFull.signal();
    }
    /**
     * Removes a single instance of the specified element from this queue,
     * if it is present.  More formally, removes an element {@code e} such
     * that {@code o.equals(e)}, if this queue contains one or more such
     * elements.
     * Returns {@code true} if this queue contained the specified element
     * (or equivalently, if this queue changed as a result of the call).
     *
     * @param o element to be removed from this queue, if present
     * @return {@code true} if this queue changed as a result of the call
     */
    public boolean remove(Object o) {
        if (o == null) return false;
        fullyLock();
        try {
            for (Node<E> trail = head, p = trail.next;
                 p != null;
                 trail = p, p = p.next) {
                if (o.equals(p.item)) {
                    unlink(p, trail);
                    return true;
                }
            }
            return false;
        } finally {
            fullyUnlock();
        }
    }
    /**
     * Returns {@code true} if this queue contains the specified element.
     * More formally, returns {@code true} if and only if this queue contains
     * at least one element {@code e} such that {@code o.equals(e)}.
     *
     * @param o object to be checked for containment in this queue
     * @return {@code true} if this queue contains the specified element
     */
    public boolean contains(Object o) {
        if (o == null) return false;
        fullyLock();
        try {
            for (Node<E> p = head.next; p != null; p = p.next)
                if (o.equals(p.item))
                    return true;
            return false;
        } finally {
            fullyUnlock();
        }
    }
    /**
     * Returns an array containing all of the elements in this queue, in
     * proper sequence.
     *
     * <p>The returned array will be "safe" in that no references to it are
     * maintained by this queue.  (In other words, this method must allocate
     * a new array).  The caller is thus free to modify the returned array.
     *
     * <p>This method acts as bridge between array-based and collection-based
     * APIs.
     *
     * @return an array containing all of the elements in this queue
     */
    public Object[] toArray() {
        fullyLock();
        try {
            int size = count.get();
            Object[] a = new Object[size];
            int k = 0;
            for (Node<E> p = head.next; p != null; p = p.next)
                a[k++] = p.item;
            return a;
        } finally {
            fullyUnlock();
        }
    }
    /**
     * Returns an array containing all of the elements in this queue, in
     * proper sequence; the runtime type of the returned array is that of
     * the specified array.  If the queue fits in the specified array, it
     * is returned therein.  Otherwise, a new array is allocated with the
     * runtime type of the specified array and the size of this queue.
     *
     * <p>If this queue fits in the specified array with room to spare
     * (i.e., the array has more elements than this queue), the element in
     * the array immediately following the end of the queue is set to
     * {@code null}.
     *
     * <p>Like the {@link #toArray()} method, this method acts as bridge between
     * array-based and collection-based APIs.  Further, this method allows
     * precise control over the runtime type of the output array, and may,
     * under certain circumstances, be used to save allocation costs.
     *
     * <p>Suppose {@code x} is a queue known to contain only strings.
     * The following code can be used to dump the queue into a newly
     * allocated array of {@code String}:
     *
     *  <pre> {@code String[] y = x.toArray(new String[0]);}</pre>
     *
     * Note that {@code toArray(new Object[0])} is identical in function to
     * {@code toArray()}.
     *
     * @param a the array into which the elements of the queue are to
     *          be stored, if it is big enough; otherwise, a new array of the
     *          same runtime type is allocated for this purpose
     * @return an array containing all of the elements in this queue
     * @throws ArrayStoreException if the runtime type of the specified array
     *         is not a supertype of the runtime type of every element in
     *         this queue
     * @throws NullPointerException if the specified array is null
     */
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        fullyLock();
        try {
            int size = count.get();
            if (a.length < size)
                a = (T[])java.lang.reflect.Array.newInstance
                    (a.getClass().getComponentType(), size);
            int k = 0;
            for (Node<E> p = head.next; p != null; p = p.next)
                a[k++] = (T)p.item;
            if (a.length > k)
                a[k] = null;
            return a;
        } finally {
            fullyUnlock();
        }
    }
    public String toString() {
        fullyLock();
        try {
            Node<E> p = head.next;
            if (p == null)
                return "[]";
            StringBuilder sb = new StringBuilder();
            sb.append('[');
            for (;;) {
                E e = p.item;
                sb.append(e == this ? "(this Collection)" : e);
                p = p.next;
                if (p == null)
                    return sb.append(']').toString();
                sb.append(',').append(' ');
            }
        } finally {
            fullyUnlock();
        }
    }
    /**
     * Atomically removes all of the elements from this queue.
     * The queue will be empty after this call returns.
     */
    public void clear() {
        fullyLock();
        try {
            for (Node<E> p, h = head; (p = h.next) != null; h = p) {
                h.next = h;
                p.item = null;
            }
            head = last;
            // assert head.item == null && head.next == null;
            if (count.getAndSet(0) == capacity)
                notFull.signal();
        } finally {
            fullyUnlock();
        }
    }
    /**
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     * @throws IllegalArgumentException      {@inheritDoc}
     */
    public int drainTo(Collection<? super E> c) {
        return drainTo(c, Integer.MAX_VALUE);
    }
    /**
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     * @throws IllegalArgumentException      {@inheritDoc}
     */
    public int drainTo(Collection<? super E> c, int maxElements) {
        if (c == null)
            throw new NullPointerException();
        if (c == this)
            throw new IllegalArgumentException();
        if (maxElements <= 0)
            return 0;
        boolean signalNotFull = false;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            int n = Math.min(maxElements, count.get());
            // count.get provides visibility to first n Nodes
            Node<E> h = head;
            int i = 0;
            try {
                while (i < n) {
                    Node<E> p = h.next;
                    c.add(p.item);
                    p.item = null;
                    h.next = h;
                    h = p;
                    ++i;
                }
                return n;
            } finally {
                // Restore invariants even if c.add() threw
                if (i > 0) {
                    // assert h.item == null;
                    head = h;
                    signalNotFull = (count.getAndAdd(-i) == capacity);
                }
            }
        } finally {
            takeLock.unlock();
            if (signalNotFull)
                signalNotFull();
        }
    }
    /**
     * Returns an iterator over the elements in this queue in proper sequence.
     * The elements will be returned in order from first (head) to last (tail).
     *
     * <p>The returned iterator is
     * <a href="package-summary.html#Weakly"><i>weakly consistent</i></a>.
     *
     * @return an iterator over the elements in this queue in proper sequence
     */
    public Iterator<E> iterator() {
        return new Itr();
    }
    private class Itr implements Iterator<E> {
        /*
         * Basic weakly-consistent iterator.  At all times hold the next
         * item to hand out so that if hasNext() reports true, we will
         * still have it to return even if lost race with a take etc.
         */
        private Node<E> current;
        private Node<E> lastRet;
        private E currentElement;
        Itr() {
            fullyLock();
            try {
                current = head.next;
                if (current != null)
                    currentElement = current.item;
            } finally {
                fullyUnlock();
            }
        }
        public boolean hasNext() {
            return current != null;
        }
        /**
         * Returns the next live successor of p, or null if no such.
         *
         * Unlike other traversal methods, iterators need to handle both:
         * - dequeued nodes (p.next == p)
         * - (possibly multiple) interior removed nodes (p.item == null)
         */
        private Node<E> nextNode(Node<E> p) {
            for (;;) {
                Node<E> s = p.next;
                if (s == p)
                    return head.next;
                if (s == null || s.item != null)
                    return s;
                p = s;
            }
        }
        public E next() {
            fullyLock();
            try {
                if (current == null)
                    throw new NoSuchElementException();
                E x = currentElement;
                lastRet = current;
                current = nextNode(current);
                currentElement = (current == null) ? null : current.item;
                return x;
            } finally {
                fullyUnlock();
            }
        }
        public void remove() {
            if (lastRet == null)
                throw new IllegalStateException();
            fullyLock();
            try {
                Node<E> node = lastRet;
                lastRet = null;
                for (Node<E> trail = head, p = trail.next;
                     p != null;
                     trail = p, p = p.next) {
                    if (p == node) {
                        unlink(p, trail);
                        break;
                    }
                }
            } finally {
                fullyUnlock();
            }
        }
    }
    /** A customized variant of Spliterators.IteratorSpliterator */
    static final class LBQSpliterator<E> implements Spliterator<E> {
        static final int MAX_BATCH = 1 << 25;  // max batch array size;
        final LinkedBlockingQueue<E> queue;
        Node<E> current;    // current node; null until initialized
        int batch;          // batch size for splits
        boolean exhausted // true when no more nodes
        long est;           // size estimate
        LBQSpliterator(LinkedBlockingQueue<E> queue) {
            this.queue = queue;
            this.est = queue.size();
        }
        public long estimateSize() { return est; }
        public Spliterator<E> trySplit() {
            Node<E> h;
            final LinkedBlockingQueue<E> q = this.queue;
            int b = batch;
            int n = (b <= 0) ? 1 : (b >= MAX_BATCH) ? MAX_BATCH : b + 1;
            if (!exhausted &&
                ((h = current) != null || (h = q.head.next) != null) &&
                h.next != null) {
                Object[] a = new Object[n];
                int i = 0;
                Node<E> p = current;
                q.fullyLock();
                try {
                    if (p != null || (p = q.head.next) != null) {
                        do {
                            if ((a[i] = p.item) != null)
                                ++i;
                        } while ((p = p.next) != null && i < n);
                    }
                } finally {
                    q.fullyUnlock();
                }
                if ((current = p) == null) {
                    est = 0L;
                    exhausted = true;
                }
                else if ((est -= i) < 0L)
                    est = 0L;
                if (i > 0) {
                    batch = i;
                    return Spliterators.spliterator
                        (a, 0, i, Spliterator.ORDERED | Spliterator.NONNULL |
                         Spliterator.CONCURRENT);
                }
            }
            return null;
        }
        public void forEachRemaining(Consumer<? super E> action) {
            if (action == null) throw new NullPointerException();
            final LinkedBlockingQueue<E> q = this.queue;
            if (!exhausted) {
                exhausted = true;
                Node<E> p = current;
                do {
                    E e = null;
                    q.fullyLock();
                    try {
                        if (p == null)
                            p = q.head.next;
                        while (p != null) {
                            e = p.item;
                            p = p.next;
                            if (e != null)
                                break;
                        }
                    } finally {
                        q.fullyUnlock();
                    }
                    if (e != null)
                        action.accept(e);
                } while (p != null);
            }
        }
        public boolean tryAdvance(Consumer<? super E> action) {
            if (action == null) throw new NullPointerException();
            final LinkedBlockingQueue<E> q = this.queue;
            if (!exhausted) {
                E e = null;
                q.fullyLock();
                try {
                    if (current == null)
                        current = q.head.next;
                    while (current != null) {
                        e = current.item;
                        current = current.next;
                        if (e != null)
                            break;
                    }
                } finally {
                    q.fullyUnlock();
                }
                if (current == null)
                    exhausted = true;
                if (e != null) {
                    action.accept(e);
                    return true;
                }
            }
            return false;
        }
        public int characteristics() {
            return Spliterator.ORDERED | Spliterator.NONNULL |
                Spliterator.CONCURRENT;
        }
    }
    /**
     * Returns a {@link Spliterator} over the elements in this queue.
     *
     * <p>The returned spliterator is
     * <a href="package-summary.html#Weakly"><i>weakly consistent</i></a>.
     *
     * <p>The {@code Spliterator} reports {@link Spliterator#CONCURRENT},
     * {@link Spliterator#ORDERED}, and {@link Spliterator#NONNULL}.
     *
     * @implNote
     * The {@code Spliterator} implements {@code trySplit} to permit limited
     * parallelism.
     *
     * @return a {@code Spliterator} over the elements in this queue
     * @since 1.8
     */
    public Spliterator<E> spliterator() {
        return new LBQSpliterator<E>(this);
    }
    /**
     * Saves this queue to a stream (that is, serializes it).
     *
     * @param s the stream
     * @throws java.io.IOException if an I/O error occurs
     * @serialData The capacity is emitted (int), followed by all of
     * its elements (each an {@code Object}) in the proper order,
     * followed by a null
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        fullyLock();
        try {
            // Write out any hidden stuff, plus capacity
            s.defaultWriteObject();
            // Write out all elements in the proper order.
            for (Node<E> p = head.next; p != null; p = p.next)
                s.writeObject(p.item);
            // Use trailing null as sentinel
            s.writeObject(null);
        } finally {
            fullyUnlock();
        }
    }
    /**
     * Reconstitutes this queue from a stream (that is, deserializes it).
     * @param s the stream
     * @throws ClassNotFoundException if the class of a serialized object
     *         could not be found
     * @throws java.io.IOException if an I/O error occurs
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in capacity, and any hidden stuff
        s.defaultReadObject();
        count.set(0);
        last = head = new Node<E>(null);
        // Read in all elements and place in queue
        for (;;) {
            @SuppressWarnings("unchecked")
            E item = (E)s.readObject();
            if (item == null)
                break;
            add(item);
        }
    }
}

原文
地址:https://www.jianshu.com/p/32d6526494fd

前言

在之前的文章中,已经对ArrayBlockingQueueLinkedBlockingQueue这两个比较常用的阻塞队列做了源码分析,我们知道其内部都是通过ReentrantLock来保证数据读写的线程安全,通过Condition来完成线程等待和唤醒,只不过ArrayBlockingQueue在读写时使用了一把锁所完成,而LinkedBlockingQueue对于读和写分别使用了两把锁来进行处理,从而达到读写分离的效果。
然而,通过锁机制来实现一个线程安全的队列,在并发不是特别高的情况下并不是非常合适,因为在大多数情况下都只有几个线程同时访问,而每次执行都需要去加一次锁,从而导致线程进行上下文切换,影响整体性能。因此,JDK还为我们提供了一个无锁线程安全的队列——ConcurrentLinkedQueue,其底层使用CAS来实现无阻塞的并发控制。本文将该队列的实现机制和源码做一个分析,让我们共同看看Doug Lea大神是如何巧妙地通过无锁机制来实现一个线程安全的队列。

1.ConcurrentLinkedQueue介绍

首先让我们看看JDK文档对该类的描述:
ConcurrentLinkedQueue的API描述.png
ConcurrentLinkedQueue是一个基于链表、无界、线程安全的队列。这个队列将元素按照先进先出的顺序进行存储。队列的头节点是在队列中存在时间最久的节点,队列的尾节点是在队列中存在时间最短的节点。新的元素会被插入到队列的尾部,而队列的元素的获取操作则会从队列的头部去获取元素。ConcurrentLinekedQueue适合作为多个线程共享访问的集合。与大多数并发集合的实现类似,该类也不允许添加null元素,该类的实现使用了一个高效的无锁算法,其算法的是基于podc-1996.pdf所改进的。
除了上面所描述的基本特性之外,ConcurrentLinkedQueue中还有一些其他的特点:

2. ConcurrentLinkedQueue内部结构分析

由于是基于链表的实现方式,与其他的并发队列类似,都会在内部定义一个节点类,ConcurrentLinedQueu亦是如此,首先我们看一下节点的定义:
//该节点是一个静态内部类,因此其只能作用于该队列内部
private static class Node<E> {
/*
* 当前节点存储的元素,注意到这里使用volatile关键字来对节点进行
* 修饰,其目的是在并发读的时候保证内存的可见性
*/
volatile E item;
//当前节点的下一个节点
volatile Node<E> next;

/**
* 构建新的节点,这里没有使用volatile的方式来对节点的元素值进行设置,而是使用普通的写方式
* 因为对于一个新增的节点,只有在其被成功插入到队列尾部才对外可见,因此在这里没有对数据可见性的强制要求
*/
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
/*
* 通过Unsafe来完成对当前节点元素的CAS操作
*/
boolean casItem(E cmp, E val) {
returnUNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
/*
* 使用普通的方式来设置当前节点的下一个节点
*/
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
/*
* 通过Unsafe来完成对当前节点的下一个节点的CAS操作
*/
boolean casNext(Node<E> cmp, Node<E> val) {
returnUNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long itemOffset;
private static final long nextOffset;

static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class k = Node.class;
//通过Unsafe来获取一个Node节点的item属性在内存中相对该对象的位置偏移量
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));

//通过Unsafe来获取一个Node节点的next节点属性在内存中相对该对象的位置偏移量
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
上面节点的定义与之前分析的SynchronousQueue中的内部定义的节点非常类似,这里不再过多阐述。
队列的属性定义
为了提高快速查找队列中第一个节点和最后一个节点,因此ConcurrentLinkedQueue中分别定义了一个head节点和tail节点来快速定位。
/**
* 不变性:
* - 队列中所有未删除的节点都可以通过head节点的succ方法查找到
* - head节点一定不可能等于null
* - (tmp = head).next != tmp,即head的next不能指向自己。
*
* 可变性:
* - head的item可能为null,也可能不为null
* - tail节点可能会滞后于head节点,因此从head节点未必一定可以找到tail节点
*
*/
private transient volatile Node<E> head;
/**
* 不变性:
* - 节点中的最后一个元素总是可以通过tail的succ方法来获取
* - tail节点不等于null
*
* 可变性:
* - head的item可能为null,也可能不为null
* - tail 节点的next可能指向自己,也可能不指向自己
* - tail节点可能会滞后于head节点,因此从head节点未必一定可以找到tail节点
*/
private transient volatile Node<E> tail;

public ConcurrentLinkedQueue() {
head = tail = new Node<E>(null);
}

ConcurrentLinkedQueue源码分析

通过上面的描述我们知道了该队列是通过一个头节点和一个尾节点,然后将中间链接节点之间两两相连接构成一个队列,下面让我们分析一下ConcurrentLinkedQueue的内部的具体实现。(在这里需要说明一下,由于其节点是完全地基于Unsafe来完成CAS的操作,如果你对该内容还不是很熟悉的话,可以参考我的深入分析Java中的原子操作这篇文章,里面对原子操作有一个比较细致的描述。

入队列源码分析

在分析源码之前,我们先通过几张图来说明一下ConcurrentLinkedQueue的入队列的整体过程。在了解完整体的过程后,再结合源码去分析细节会更加容易理解:
队列初始化
队列初始化状态
在队列刚创建时,head和tail同时指向空节点,我们也称其为dummy节点。
dummy节点的说明
添加元素a
添加元素a
向队列中添加元素a,此时只是新节点追加到第一个节点的后面,但是tail节点并未发生改变。
添加元素b
添加元素b
向队列中添加元素b,此时新节点与原先的tail节点之间的距离大于1,因此tail节点在这个时候会更新,真正的指向了最后一个节点
添加元素c
添加元素c
由于新节点与tail节点的距离没有大于1,因此此时tail节点同样不会发生更新。
添加元素d
添加元素d
通过上面的图示,我们可以看到ConcurrentLinkedQueue在入队列过程中非常明显的一个特点就是tail指针不是实时更新的,即tail节点可能会滞后于队列中真正的最后一个节点,只有当最后的一个节点与tail节点之前的距离大于1时才会更新,而这样设计的目的就是为了减少避免每增加一个节点,tail节点都需要去执行一次CAS操作的情况发生。
public boolean offer(E e) {
// 由于元素不允许为null,因此对元素做一个检查
checkNotNull(e);
// 生成待插入的节点
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
// 当q等于null时,则p就是队列中的最后一个元素
if (q == null) {
// 执行cas操作,如果执行成功,则newNode成为队列的最后一个元素,但它未必是tail指向的元素
if (p.casNext(null, newNode)) {
// 通过判断p!=t,从而确定当前新节点与tail指向的节点之间的距离是否大于1
// 如果大于1,则需要更新tail指针
if (p != t)
casTail(t, newNode);
return true;
}
}
// 如果p==q,则意味着当前p节点已经被从队列中移除(如果单纯从入队列看是看不出来的,后面结合出队列再回头分析)
else if (p == q)
/* 判断在执行过程中tail是否发生变化,如果未发生变化,则tail也已经脱落队列
* 因为 p = t = tail,而p已经脱离队列,从而推断出tail也脱离了队列
* 那么此时只能从head开始,重新查找队列的最后一个元素
* 如果tail发生了变化,则直接从当前队列的tail开始查找队列的最后一个元素
*/
p = (t != (t = tail)) ? t : head;
else
/**
* 由于p节点的next不为null,并且p节点并未从队列中删除,因此需要继续查找队列的最后一个节点
* 判断执行过程中tail节点是否发生了变化
* 如果发生了变化,则让p执行当前的tail,否则就让p直接指向它的next节点q
*/
p = (p != t && t != (t = tail)) ? t : q;
}
}
可能会有不少朋友对上面t != (t = tail)的处理感到疑惑,疑惑的原因可能会觉得一个变量自己和自己比较,那不是一定为true嘛,怎么还会出现等于false的可能呢。为了理解这个问题,我们一起看一下下面的这段代码:
public class VarCompareTest {
static volatile int b = 2;

public static void main(String[] args) {
int a = b;
int c = a != (a = b) ? 5 : 4;
System.out.println(a);
System.out.println(c);
}
}
这段代码的也用到了上面类似的t != (t = tail),但是在编译完成后,我们通过IDEA本身的反编译工具来查看一下对应的Class文件的结果(目前由于对字节码指令不熟悉,因此不从那个角度去解读):
反编译Class文件后的结果
通过上面的代码,我们可以看到,一开始a的值就等于b的值,其值为2;紧接着a的值首先通过一个局部变量记录,然后再将b的值赋值给a,由于b可能存在多线程修改的可能,此时b的值可能被其他线程改成了3,因此a的值也会变为3,最后再拿2与3进行比较,即var10000 != b进行比较,就会存在等于false的情况发生了。对于t != (t = tail)的情况也是如此,相信通过这个例子说明大家应该明白其中的原理了!

出队列源码分析

在分析出队列源码之前,我们也结合上面的图来看一下出队列的整体过程:
队列当前状态
这里假设队列是基于上面入队列之后的状态进行的。
移除第1个节点
移除第一个节点
原本指向head的节点,此时的next指向了自己,因此它从队列中真正的移除。存储a的节点,其item置为了null,并且head节点发生变更,真正指向了队列中第一个有效(真正存储数据)的节点。
移除第2个节点
移除第2个节点
此时只是仅仅将节点的item设置为了null,但是head节点没有发生变更,这样做的目的也是为了减少一次CAS操作,它会等到下一次才去变更。
移除第3个节点
移除第3个节点
移除第4个节点
移除第4个节点
看完上图的分析,相信大家对ConcurrentLinkedQueue出队列的操作应该有一个直观的理解了,下面我们看下源码的具体实现:
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 如果元素的item不为null,则说明p节点是当前队列中的第一个未被删除的节点
// 此时也说明head指向的节点确实是队列中的第一个元素
// 通过CAS操作,将item设置为null,来标记其已经被删除
if (item != null && p.casItem(item, null)) {
// 判断p节点与head指向的节点是否是同一个,如果不是则需要将head节点向前移动
if (p != h)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
// 节点的next为null,则说明队列为空,只有一个dummy节点
else if ((q = p.next) == null) {
// 尝试将dummy节点p设置为新的head
updateHead(h, p);
return null;
}
// 如果p节点被删除,只能从队列的head从头开始再次查找
else if (p == q)
// 跳回到最外层的循环,重新执行一次Node<E> h = head, p = h, q;操作
continue restartFromHead;
else
// 由于head指向的节点其item为null,即head指向的节点不是一个有效节点,因此继续通过head的next继续查找
p = q;
}
}
}

final void updateHead(Node<E> h, Node<E> p) {
// 判断新设置的节点与原来的head节点是否是同一个,如果不是则将新节点设置为新的head节点,
// 并且将原来的head节点的next指向自己。
if (h != p && casHead(h, p))
h.lazySetNext(h);
}

tail节点滞后于head节点的场景分析

一般来说,head节点都在队列的左边,而tail节点在队列的右边。然而,在ConcurrentLinkedQueue中,可能存在tail节点在左边,而head节点却跑到了右边的情况,这种场景我们将其称为tail lag behind head。下面我们分析一下在什么样的场景下会发生这样的情况,这对于真正理解ConcurrentLinkedQueue非常重要!
添加一个元素
从上图可以看到,在添加完一个元素后,tail节点并有发生改变。此时,假设我们去获取队列中的元素,队列的结构就变成如下的样子。
取出队列元素
从上面的结构我们可以看到,此时tail节点滞后于head节点,并且我们此时通过head节点也无法查找到tail节点,因为该节点已经从队列中移除。当下一次添加元素的时候,就会出现tail节点自己指向自己的情况,此时就需要重新获取到head,将新增的元素追加到head后面。

总结

至此,ConcurrentLinkedQueue的实现我们已经分析完成了。该类的核心设计就在于CAS的无阻塞以及head/tail节点的延迟更新。尽量我们在实际的开发中基本不会去实现一个如此复杂的队列,但是通过分析一个经典无阻塞队列,可以更加好地帮助我们理解并发编程。如果存在分析不对的地方,还望大神指出。


作者:码农一枚
链接:https://www.jianshu.com/p/32d6526494fd
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

个人理解
GC里面的准确分类有两种:
针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
  • Partial GC:并不收集整个GC堆的模式
    • Young GC:只收集young gen的GC
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC(或者说major gc):收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式,除了G1收集器和CMS收集器以外的old gc发生时实际上都会伴随young gc,所以这种都是Full GC。
这种分类方式是深入理解JVM虚拟机当中的分类不太一样,不过却更加科学。
G1其实包括三种收集模式:
  • young GC(回收eden和部分survivor)
  • Mixed GC (回收所有新生代和部分老年代)
  • FULL GC (G1出现问题,回收整个堆空间)
摘抄
作者:RednaxelaFX
链接:https://www.zhihu.com/question/41922036/answer/93079526
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。
最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:
HotSpot VM里其它非并发GC的触发条件复杂一些,不过大致的原理与上面说的其实一样。
当然也总有例外。Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,并且两次GC之间能让应用程序稍微运行一小下,以期降低full GC的暂停时间(因为young GC会尽量清理了young gen的死对象,减少了full GC的工作量)。控制这个行为的VM参数是-XX:+ScavengeBeforeFullGC。这是HotSpot VM里的奇葩嗯。可跳传送门围观:JVM full GC的奇怪现象,求解惑? - RednaxelaFX 的回答
并发GC的触发条件就不太一样。以CMS GC为例,它主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收


不管是java7的永久代,还是java8的元空间,方法区存储的东西都差不多(字符串常量池有变化,intern()方法存放堆中字符串对象的引用)。
这儿更正一下自己以前犯的错误:
方法区(永久代或者元空间 并没有保存类实例的具体信息(class对象),也没有反射对象(method、field等对象),这些内容都保存在常规的堆空间内。永久代和元空间内保存的信息只对编译器和JVM的运行时有用,这部分被称为“类的元数据”。
简单理解就是class对象和反射对象不过是一些为java服务的对象(含有一部分类数据,不是全部),但是对于编译和运行时必须的类数据(java语言概念之下的数据)是存储在的方法区当中的,这部分只和编译器和JVM有用。

持续可用和快速容灾切换的能力,是技术人员追求的极致目标。在架构设计中,容灾设计强调的是系统对外界环境影响具备快速响应能力,节点级别的快速恢复能力,保障系统的持续可用。
去年12月18日,全球架构师峰会上,阿里巴巴高级系统工程师曾欢(善衡)结合互联网金融业务及系统特性,分享了在支付宝系统架构演进中,每个阶段的高可用和容灾能力建设的解决思路。

高可用和容灾架构的意义

企业服务、云计算、移动互联网领域中,高可用的分布式技术为支撑平台正常运作提供着关键性的技术支撑。从用户角度,特别是作为主要收入来源的企业用户的角度出发,保证业务处理的正确性和服务不中断(高可用性)是支撑用户信心的重要来源。高性能,高可用的分布式架构就成了访问量高峰期时,网站得以成功运维的关键。
在当今信息时代,数据和信息逐渐成为各行各业的业务基础和命脉。当企业因为信息化带来快捷的服务决策和方便管理时,也必须面对着数据丢失的危险。
容灾系统,作为为计算机信息系统提供的一个能应付各种灾难的环境,尤其是计算机犯罪、计算机病毒、掉电、网络/通信失败、硬件/软件错误和人为操作错误等人为灾难时,容灾系统将保证用户数据的安全性(数据容灾),甚至,一个更加完善的容灾系统,还能提供不间断的应用服务(应用容灾)。可以说,容灾系统是数据存储备份的最高层次。
每年的“双11”、“双12”都是全球购物者的狂欢节,今年的双11有232个国家参与进来,成为名副其实的全球疯狂购物节。11月11日,全天的交易额达到912.17亿元,其中在移动端交易额占比68%今年每秒的交易峰值达到14万笔,蚂蚁金服旗下的支付宝交易峰值达到8.59万笔/秒,这一系列的数字,考验的是支付宝背后强大的IT支持能力。而持续可用和快速容灾切换的能力,是支付宝技术人员追求的极致目标。
在架构设计中,作为系统高可用性技术的重要组成部分,容灾设计强调的是系统对外界环境影响具备快速响应能力,尤其是当发生灾难性事件并对IDC节点产生影响时,能够具备节点级别的快速恢复能力,保障系统的持续可用。2015年12月18日,年度高端技术盛会:“全球架构师峰会——ArchSummit”在北京国际会议中心隆重召开,会上,阿里巴巴高级系统工程师:善衡(曾欢)结合互联网金融业务及系统特性,分享了在支付宝系统架构演进中,每个阶段的高可用和容灾能力建设的解决思路。本文由其演讲内容整理而成。
支付宝的系统架构,其发展历程可以分为清晰的3个阶段,每一个阶段都有自己独特的特点和架构上相应的痛点。在每一个阶段的发展过程中,支付宝的技术人员针对不同的问题进行诸多的思考,在解决这些问题的过程中也做了诸多的尝试。

纯真:童年时期2004年~2011年

在此阶段,支付宝的系统架构相对比较简化,如图1所示,通过商用LB让用户的流量进到入口网关系统,支付宝的系统服务暴露也通过商用设备挂在VIP下,每个提供服务的应用机器通过VIP来进行负载均衡。早期支付宝的核心系统库都在一个数据库上(后期拆为多个数据库),即每个核心系统都只用单独的数据库。在这样一个“物理上多机房,逻辑上单机房”的架构背后,每天的业务量仅仅为数十万级,应用系统也只有数十个,容灾能力相对较低:例如单应用出现问题时无法及时有效地切换、主机和备用机进行切换时,一定程度上会导致业务中断,甚至有时会有不得不进行停机维护的情况,使得整个系统面对数据故障时显得十分被动。
随着业务量的不断增长,该架构发展到2011年,出现了一些比较典型的问题。如下图所示:由于系统内部使用的都是LB设备,商用LB的瓶颈就尤其明显,由于业务的发展累计,VIP及其上面发布的服务越堆越多,设备如果出现抖动或者宕机会对业务造成严重影响,这即是架构上的单点。第二个问题就是数据库的单点瓶颈。随着业务量的不断增加,单一的核心数据库一旦出现异常,比如硬件故障、负载异常等等,进而会导致整个核心链路上的业务都不可用。
如何消除系统架构当中存在的单点问题,优雅解决未来1-3年之间业务量增长(数百万级/天)和机器数量增长(数百个系统),是首先要解决的问题,于是带来了下一代架构革新。

懵懂:少年时期2011年~2012年

鉴于第一阶段支付宝碰到的这些痛点,在第二个阶段,它将逻辑上的单个机房拆分成为多个机房,通过把硬负载转换成为软负载,以实现分布式的服务调用,如下图所示。下图为基于常见的消费者和生产者模型来构建的业务模型,其中配置中心负责服务注册以及对应服务提供方可用状态变化的通知,从而将信息实时推送到消费方的订阅关系上。值得注意的是,支付宝对原有架构做了一个较大的改进:它将普通的一体化配置中心分拆成两个模块,一个Session模块,用于管理消费者和生产者的长连接保持;一个Data模块,用于注册服务时存储相关。通过这两个模块的深度解耦,进一步提高整个配置中心的性能。
除此之外,支付宝还做了数据的水平扩展。其实早在2011年之前,支付宝就已经开始从事此项工作,根据用户的UID,将一个交易库水平拆分为多个库,如下图所示。在此期间,需要解决的问题就是“对应用透明”,如何通过“应用无感知”的方式进行用户数据库的拆分,是水平扩展实施时首先要考虑的;其次,如何通过一个数据中间件来管理数据分片的规则,也是数据水平扩展过程中的关键问题。
通过上述两个比较典型的优化,支付宝的整个系统架构如图5所示。从应用层面上来说每个机房都是一个节点;从数据层面上,每个机房有特定的几个数据分片在里面。在部署模式上,当支付宝扩张到3个或4个机房的时候,需要全局考虑数据分片部署,每个机房都有可能不可用,这些备库们应当分散到不同的多个机房里面,从而达到多机房备灾的目的。
这种多机房多活的第二代架构体系,其优势在于:
1.进行了数据的水平拆分,从而理论上可以无限扩展数据/资源;
2.应用多机房独立部署,不再存在单个机房影响整体的情况;
3.服务调用机房内隔离,通过软负载的方式去做机房内的服务本地化,不再去依赖另一个机房内相同的服务;
4.相较上一个阶段具有更高、更可靠的可用性。
虽然在此过程中自然解决了上述的单点问题,但仍存在一个问题没有解决。即数据库日常维护时,或因为硬件的故障导致数据库宕机时,支付宝需要进行主备切换,在此过程中业务是有损的状态。一般情况下当IDC出现问题的时候,工程师们会通过前端的流量管控系统先把用户的流量从出现异常的机房切换到正常的机房中,然后进行数据库的主备切换,即将备用负载替换主用负载。
在这个过程中,有两个比较大的痛点:
1.主备切换时数据“一致性”的问题,即主备切换时,如何在把影响时间降至最低的情况下,保证数据不被丢失,完完整整地拷贝至备用数据库。
2.主备切换时数据存取不可用导致的业务暂停问题。
一旦数据库发生故障,我们需要进行主备切换时,因为切换过程中的数据不可写,部分用户操作后的状态不对,对用户来说是会担心的。为了解决这个问题,我们制定了一个Failover方案。该方案主要通过业务层进行改造,例如针对流水型的业务数据,我们是这么来做的:正常进行数据流量存取的时候,只有主库提供路线服务,主库和备库之间进行正常的数据同步,但是Failover库不进行数据同步,正常状态下对业务系统不可见。即正常情况下,没有数据流经过Failover库,而且Failover库是空的,没有任何历史数据,如下图所示:
一旦故障发生,主库发生宕机,支付宝人员将通过容灾切换将所有数据的读写放置在FailOver数据层中进行。因为是实时流水型的数据,所以不会对历史数据产生任何依赖和影响。切换完成后,整个核心链路上的业务就已经全面恢复起来了。通过这种方式,使得支付宝可以将数据库在短短5分钟内进行切换,一旦故障解除,随时可以将数据读写重新切换到主存储库上来。
FailOver方案上线后,支付宝基本上所有的核心业务都基于此进行了方案改造,并针对不同的业务(不仅限于应用层)都会有不同的FailOver方案。现在,支付宝在原有的方案基础上再多准备一个Failover数据库(如图8中蓝色图形所示),与之前提到的容灾设计一样,如果需要进一步保证Failover库的可用性,还可以增加Failover库的备库。此外Failover库需要与主库分开,可以与主库、备库都不放在同一个机房。

成熟:青年时期2012年~2015年

通过“多机房多活”的架构改造以及FailOver方案的实施,满以为未来三年都无需为IDC资源去发愁的支付宝研发团队,在顺利支撑完2012年的“双11”,为下一年做规划时却发现梦想破灭了。因为他们遇到了严峻的新问题:
1.DB连接数不够。由于一个机房中的应用系统就是一个节点,没有分片的概念,仅仅只有数据的分片。用户进入任意应用节点时,系统需要根据用户的UID分片查用户应该去往哪个数据库,这时候每个应用节点都会与所有数据库节点保持连接。而传统关系型数据库的连接数是十分宝贵的,当支付宝面对不断增长的用户扩容需求时,因为DB连接数资源无法继续扩充,导致应用也不能继续扩容了,不仅无法满足日常增长需求,更别提“双11”这样短时间爆发式增长的用户需求。
2.城市IDC资源限制问题。2012年夏天,杭州经历了长时间高温天气,由于机房运行的耗电较高,市政为了缓解电力供应压力,下达了一纸通文,随时可能关闭掉支付宝的某些机房供电。
3.机房间高流量问题。由于一个业务请求与后端数据读取之间的比例为1:N(其中N可能是几十甚至上百),在“双11”高流量的冲击下,跨机房的流量会变得非常的大,因此对于网络带宽和网络质量也有着非常高的要求。
4.跨IDC网络延时问题。由于业务请求和后端数据读取1:N配比问题,导致同城机房之间距离稍微远一些,就会产生1、2毫秒的同城机房延时,被扩大后就会成为困扰用户的网络延时问题。
新的问题进而带来对新一代架构的要求:
1.彻底解决DB连接数的问题。
2.彻底解决IDC资源限制的问题。需要支付宝走出杭州,不能单单在杭州进行机房的扩张和建设。
3.保证业务的连续性。去减少故障发生的时候对于用户的打扰、对于业务的中断。
4.蓝绿发布。在日常发布时,会通过线下的发布测试,预发布,最终到线上的发布过程。线上发布通常采用的是金丝雀模式,应用分成多组进行发布,每一组的用户不固定,可能会影响到大部分乃至全站的用户。因此支付宝团队希望日常发布时,能够最小限度影响用户(可能是1%甚至1‰),新代码上线之后最小力度来验证新代码符合预期。因此需要一种新的发布方式来支持。
5.高可用-异地多活。对于支付宝来说,传统的“两地三中心”要求:机房分属两个不同地区,同城当中两个机房是“双活”,即活跃的主机房,异地机房通过复制数据来做“冷备”,即备用机房。若同城两个机房都出现问题,一旦需要切换异地机房,由于不知道异地冷备机房运行起来是什么情况,因此很难决策。支付宝希望异地的机房也能实时进行流量的读写,以便数据流量可以来回切换,而不用担心在切换后无法知道数据将是什么状态。
6.    单元化。基于上述几个问题的思考,支付宝走到了单元化的一步。如图9所示,传统的服务化架构下每一次调用服务时,系统都会随机选择一台机器或一个节点完成整次的调用。而单元化之后支付宝可以把任何一个节点固定到一个单独的单元内,即固定的节点对应一条固定的链路,再基于数据分片的方法完成将节点“单元化”的设置。
单元化的核心思想包括核心剥离以及长尾独立。核心剥离指的是将核心业务进行剥离;将业务数据按照UserID进行拆分,从而实现多机房部署;在此基础上将每一个机房的调用进行封闭式设置;每一个单元中的业务数据无需和其它单元进行同步。长尾独立则针对非核心的应用,这些业务数据并不按照UID进行拆分,核心业务并不依赖于长尾应用。
支付宝单元化架构实现的主要思想有两点,如下图所示:
数据水平拆分,即将所有线上核心业务分离开来。由于核心业务集合都能按照用户ID去做水平切片,支付宝团队将数据切片后按照机房进行分布,然后通过循序渐进的方式逐步将每个单元之间的调用完整地封闭掉。每一个单元的数据都是经过分片的数据,不需和其它单元进行同步。
上层单元化改造,即将历史的、不能拆分的业务独立出来。2013年,支付宝实现了两个单元:单元A放置核心业务,例如核心链路的支付、交易等;单元B放置无法拆分的历史业务,例如某些不重要的业务等。
支付宝单元化架构于2013年完成,帮助支付宝顺利支撑过了2013年的“双11”。而2014年~2015年,支付宝一直在尝试解决异地延时的问题:即如果不去对全局数据进行分割或是本地化的话,当把业务单元搬到异地时,由于每一次业务的发生基本上都会依赖全局数据,且会对应多次数据访问,而每一次数据访问都会产生异地所带来的延时,积少成多,时间延迟就会达到秒级,用户体验大幅度下降。基于这个问题的考虑,支付宝研发团队做了一个很大的决定:他们引入了单元C,通过将无法拆分的全量数据进行全局复制(异地机房之间的复制),以便于支撑核心业务本地调用,进而实现读写分离,将跨城市的调用减少到最小力度。如下图所示。
由于数据的写入操作会在每一个数据单元上进行,而单元C的数据读取是要求全量的,所以进行这一项改造的时候需要底层的支持,从而解决架构改造时数据一致性和时效性的问题。为了解决这个问题,支付宝团队提出了两种解决方案:
1.基于DB同步的数据复制。针对某些对于延时并非十分敏感的业务,单就基于主备同步来做数据的同步。
2.基于消息系统的数据复制。由于异地数据同步比较耗时,对于延时非常敏感的业务来说,支付宝基于可靠的消息系统做一个数据复制(内部称为数据总线),通过它将上层基于应用去做数据的复制,大概时间位于毫秒级。底层DB主备同步同时进行。
通过本地化调用,支付宝有效地解决了DB连接数问题、机房间流量限制问题、跨IDC的延时问题;通过异地的单元部署以及不断的容灾演练和数据切换,支付宝有效地解决了城市IDC资源限制和异地IDC容灾问题。另外还有一个诉求——“蓝绿发布”,支付宝是这么实现的。
如下图所示,蓝绿发布即每个单元里面分为一个Blue组和一个Green组,日常模式下两个组各承担50%的用户流量;发布前将Green组中的50%流量移到Blue组中,然后对Green进行两批无序发布;新代码发布上去后,将Blue组中的流量先切换1%~2%到Green组中进行验证,以最大程度减少对用户的打扰,验证完毕后,再逐步将100%的流量全部切换至新的Green组,重复前面的步骤发布Blue组,验证完毕后切回每个组各50%流量的日常模式。
至此,支付宝单元化全局架构如下图所示。每一个机房单元中有单元A和单元C,单元A中按逻辑分为了两个组,单元C之间进行全局的数据复制;每个单元中部署了一套分布式的容灾管控系统,使得支付宝能在单个机房故障的时候去做更加快速的应对。

总结

通过单元化的系统配置,使得支付宝整个系统架构具有很强的高可用性和容灾能力。具体来说有三点:
1.更灵活的流量管控,可以实现以更小的力度和更快的速度来进行数据切换。
2.自定义化的数据流量分配。由于每一个数据单元需要多少资源、需要支撑多少交易量可以前期确定,因此支付宝可以按照实际的需求量进行单元维度的扩展。
3.快速恢复的容灾能力。通过单元化之后,不仅使得数据单元内Blue组和Green组之间可以切换流量,再向上一个级别,单元和单元之间、机房和机房之间、同城内数据中心之间甚至城市和城市之间也可以自如地进行故障发生时的应急切换。


作者:java高级分享
链接:https://www.jianshu.com/p/ceb837bb737c
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

深入理解G1垃圾收集器

G1 GC是Jdk7的新特性之一、Jdk7+版本都可以自主配置G1作为JVM GC选项;作为JVM GC算法的一次重大升级、DK7u后G1已相对稳定、且未来计划替代CMS、所以有必要深入了解下:
不同于其他的分代回收算法、G1将堆空间划分成了互相独立的区块。每块区域既有可能属于O区、也有可能是Y区,且每类区域空间可以是不连续的(对比CMS的O区和Y区都必须是连续的)。这种将O区划分成多块的理念源于:当并发后台线程寻找可回收的对象时、有些区块包含可回收的对象要比其他区块多很多。虽然在清理这些区块时G1仍然需要暂停应用线程、但可以用相对较少的时间优先回收包含垃圾最多区块。这也是为什么G1命名为Garbage First的原因:第一时间处理垃圾最多的区块。
平时工作中大多数系统都使用CMS、即使静默升级到JDK7默认仍然采用CMS、那么G1相对于CMS的区别在:
  1. G1在压缩空间方面有优势
  2. G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
  3. Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
  4. G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
  5. G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
  6. G1会在Young GC中使用、而CMS只能在O区使用
就目前而言、CMS还是默认首选的GC策略、可能在以下场景下G1更适合:
  1. 服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
  2. 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
  3. 想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象

一次完整G1GC的详细过程:

G1在运行过程中主要包含如下4种操作方式:
  1. YGC(不同于CMS)
  2. 并发阶段
  3. 混合模式
  4. full GC (一般是G1出现问题时发生)

YGC:

下面是一次YGC前后内存区域是示意图:
图中每个小区块都代表G1的一个区域(Region),区块里面的字母代表不同的分代内存空间类型(如[E]Eden,[O]Old,[S]Survivor)空白的区块不属于任何一个分区;G1可以在需要的时候任意指定这个区域属于Eden或是O区之类的。
G1 YoungGC在Eden充满时触发,在回收之后所有之前属于Eden的区块全变成空白。然后至少有一个区块是属于S区的(如图半满的那个区域),同时可能有一些数据移到了O区。
目前淘系的应用大都使用PrintGCDetails参数打出GC日志、这个参数对G1同样有效、但日志内容颇为不同;下面是一个Young GC的例子:
23.430: [GC pause (young), 0.23094400 secs]
...
[Eden: 1286M(1286M)->0B(1212M)
Survivors: 78M->152M Heap: 1454M(4096M)->242M(4096M)]
[Times: user=0.85 sys=0.05, real=0.23 secs]
上面日志的内容解析:Young GC实际占用230毫秒、其中GC线程占用850毫秒的CPU时间
E:内存占用从1286MB变成0、都被移出
S:从78M增长到了152M、说明从Eden移过来74M
Heap:占用从1454变成242M、说明这次Young GC一共释放了1212M内存空间
很多情况下,S区的对象会有部分晋升到Old区,另外如果S区已满、Eden存活的对象会直接晋升到Old区,这种情况下Old的空间就会涨

并发阶段:

一个并发G1回收周期前后内存占用情况如下图所示:
从上面的图表可以看出以下几点:
1、Young区发生了变化、这意味着在G1并发阶段内至少发生了一次YGC(这点和CMS就有区别),Eden在标记之前已经被完全清空,因为在并发阶段应用线程同时在工作、所以可以看到Eden又有新的占用
2、一些区域被X标记,这些区域属于O区,此时仍然有数据存放、不同之处在G1已标记出这些区域包含的垃圾最多、也就是回收收益最高的区域
3、在并发阶段完成之后实际上O区的容量变得更大了(O+X的方块)。这时因为这个过程中发生了YGC有新的对象进入所致。此外,这个阶段在O区没有回收任何对象:它的作用主要是标记出垃圾最多的区块出来。对象实际上是在后面的阶段真正开始被回收
G1并发标记周期可以分成几个阶段、其中有些需要暂停应用线程。第一个阶段是初始标记阶段。这个阶段会暂停所有应用线程-部分原因是这个过程会执行一次YGC、下面是一个日志示例:
50.541: [GC pause (young) (initial-mark), 0.27767100 secs]
[Eden: 1220M(1220M)->0B(1220M)
Survivors: 144M->144M Heap: 3242M(4096M)->2093M(4096M)]
[Times: user=1.02 sys=0.04, real=0.28 secs]
上面的日志表明发生了YGC、应用线程为此暂停了280毫秒,Eden区被清空(71MB从Young区移到了O区)。
日志里面initial-mark的字样表明后台的并发GC阶段开始了。因为初始标记阶段本身也是要暂停应用线程的,
G1正好在YGC的过程中把这个事情也一起干了。为此带来的额外开销不是很大、增加了20%的CPU,暂停时间相应的略微变长了些。
接下来,G1开始扫描根区域、日志示例:
50.819: [GC concurrent-root-region-scan-start]
51.408: [GC concurrent-root-region-scan-end, 0.5890230]
一共花了580毫秒,这个过程没有暂停应用线程;是后台线程并行处理的。这个阶段不能被YGC所打断、因此后台线程有足够的CPU时间很关键。如果Young区空间恰好在Root扫描的时候
满了、YGC必须等待root扫描之后才能进行。带来的影响是YGC暂停时间会相应的增加。这时的GC日志是这样的:
350.994: [GC pause (young)
351.093: [GC concurrent-root-region-scan-end, 0.6100090]
351.093: [GC concurrent-mark-start],0.37559600 secs]

GC暂停这里可以看出在root扫描结束之前就发生了,表明YGC发生了等待,等待时间大概是100毫秒。
在root扫描完成后,G1进入了一个并发标记阶段。这个阶段也是完全后台进行的;GC日志里面下面的信息代表这个阶段的开始和结束:
111.382: [GC concurrent-mark-start]
....
120.905: [GC concurrent-mark-end, 9.5225160 sec]
并发标记阶段是可以被打断的,比如这个过程中发生了YGC就会。这个阶段之后会有一个二次标记阶段和清理阶段:
120.910: [GC remark 120.959:
[GC ref-PRC, 0.0000890 secs], 0.0718990 secs]
[Times: user=0.23 sys=0.01, real=0.08 secs]
120.985: [GC cleanup 3510M->3434M(4096M), 0.0111040 secs]
[Times: user=0.04 sys=0.00, real=0.01 secs]
这两个阶段同样会暂停应用线程,但时间很短。接下来还有额外的一次并发清理阶段:
120.996: [GC concurrent-cleanup-start]
120.996: [GC concurrent-cleanup-end, 0.0004520]
到此为止,正常的一个G1周期已完成–这个周期主要做的是发现哪些区域包含可回收的垃圾最多(标记为X),实际空间释放较少。

混合GC:

接下来G1执行一系列的混合GC。这个时期因为会同时进行YGC和清理上面已标记为X的区域,所以称之为混合阶段,下面是一个混合GC执行的前后示意图:
像普通的YGC那样、G1完全清空掉Eden同时调整survivor区。另外,两个标记也被回收了,他们有个共同的特点是包含最多可回收的对象,因此这两个区域绝对部分空间都被释放了。这两个区域任何存活的对象都被移到了其他区域(和YGC存活对象晋升到O区类似)。这就是为什么G1的堆比CMS内存碎片要少很多的原因–移动这些对象的同时也就是在压缩对内存。下面是一个混合GC的日志:
79.826: [GC pause (mixed), 0.26161600 secs]
....
[Eden: 1222M(1222M)->0B(1220M)
Survivors: 142M->144M Heap: 3200M(4096M)->1964M(4096M)]
[Times: user=1.01 sys=0.00, real=0.26 secs]
上面的日志可以注意到Eden释放了1222MB、但整个堆的空间释放内存要大于这个数目。数量相差看起来比较少、只有16MB,但是要考虑同时有survivor区的对象晋升到O区;另外,每次混合GC只是清理一部分的O区内存,整个GC会一直持续到几乎所有的标记区域垃圾对象都被回收,这个阶段完了之后G1会重新回到正常的YGC阶段。周期性的,当O区内存占用达到一定数量之后G1又会开启一次新的并行GC阶段.
原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: 深入理解G1垃圾收集器


unsafe 关键字表示不安全上下文,该上下文是任何涉及指针的操作所必需的。 有关详细信息,请参阅不安全代码和指针
可在类型或成员的声明中使用 unsafe 修饰符。 因此,类型或成员的整个正文范围均被视为不安全上下文。 以下面使用 unsafe 修饰符声明的方法为例:
unsafe static void FastCopy(byte[] src, byte[] dst, int count)
{
// Unsafe context: can use pointers here.
}
不安全上下文的范围从参数列表扩展到方法的结尾,因此也可在以下参数列表中使用指针:
unsafe static void FastCopy ( byte* ps, byte* pd, int count ) {...}
还可以使用不安全块从而能够使用该块内的不安全代码。 例如:
unsafe
{
// Unsafe context: can use pointers here.
}
若要编译不安全代码,必须指定 /unsafe 编译器选项。 不能通过公共语言运行时验证不安全代码。

示例

C#
// compile with: /unsafe

class UnsafeTest
{
// Unsafe method: takes pointer to int:
unsafe static void SquarePtrParam(int* p)
{
*p *= *p;
}

unsafe static void Main()
{
int i = 5;
// Unsafe method: uses address-of operator (&):
SquarePtrParam(&i);
Console.WriteLine(i);
}
}
// Output: 25

C# 语言规范

有关详细信息,请参阅 C# 语言规范。 该语言规范是 C# 语法和用法的权威资料。

另请参阅

C# 参考
C# 编程指南
C# 关键字
fixed 语句
不安全代码和指针
固定大小的缓冲区


个人总结
1.错误日志 log_error
2.所有操作的日志(读和写都有) general_log
3.慢查询日志 slow_query_log_file
4.用于数据备份的二进制日志 binlog
5.事务日志(支持事务相关)
6.中继日志(主从备份中从库从主库拿到的日志)
原文
MySQL日志管理:https://segmentfault.com/a/1190000003072237

参照:系统是CentOS6.4 ,MySQL版本为5.5.32
MySQL 服务器上一共有六种日志:错误日志,查询日志,慢查询日志,二进制日志,事务日志,中继日志。

一 错误日志

错误日志不仅仅记录错误信息,它记录的事件有:
- 服务器启动和关闭过程中的信息
- 服务器运行过程中的错误信息
- 事件调度器运行一个事件时产生的信息
- (如果被配置为从服务器)启动从服务器进程时产生的信息

查看错误日志文件的路径

在mysql数据库中,错误日志功能是默认开启的。
mysql> SHOW VARIABLES LIKE 'log_error%';
+---------------+-----------------------------------------------+
| Variable_name | Value |
+---------------+-----------------------------------------------+
| log_error | /opt/lampstack-5.4.22-0/mysql/data/mysqld.log |
+---------------+-----------------------------------------------+

配置错误日志

编辑 my.cnf(一般在mysql目录下),修改 log-error 参数,如果没有就新增:
错误日志文件配置
# Error Logging.log-error="filename.log"

二 查询日志

其中查询日志记录查询操作,默认情况下查询日志是关闭的。开启查询日志会增加很多磁盘 I/O, 所以如非出于调试目的,不建议开启查询日志(不只是查询,所有操作的日志都有)

查看查询日志是否启用及查询日志的路径

mysql> SHOW VARIABLES LIKE 'general_log%';
+------------------+--------------------------------------------------+
| Variable_name | Value |
+------------------+--------------------------------------------------+
| general_log | OFF |
| general_log_file | /opt/lampstack-5.4.22-0/mysql/data/localhost.log |
+------------------+--------------------------------------------------+

配置查询日志

编辑 my.cnf,修改 general-log 参数为 1,同时设定 log-output 参数(日志输出类型)和 general_log_file 参数(查询日志路径):
# General logging.:log-output=FILE
general-log=1general_log_file="filename.log"
保存 my.cnf 更改,重启 MySQL 服务。

三 慢查询日志

慢查询是指执行时长(包括等待CPU/IO的时间)超过 long_query_time 这个变量定义的时长的查询。慢查询日志开销比较小,可以用于定位性能问题,建议开启。

查看慢查询日志是否启用及慢查询日志的路径

mysql> SHOW VARIABLES LIKE 'slow_query_log%';
+---------------------+--------------------------------------------------+
| Variable_name | Value |
+---------------------+--------------------------------------------------+
| slow_query_log | OFF |
| slow_query_log_file | /usr/local/var/mysql/upstreamdeMac-mini-slow.log |
+---------------------+--------------------------------------------------+

配置慢查询日志

编辑 my.cnf ,设置 log_slow_queries 参数为 1,同时设定 log-output 参数(日志输出类型)、slow-query-log_file 参数(慢查询日志路径)和 long_query_time 参数:
# Slow logging.log-output=FILE
log_slow_queries=1 //MySQL 5.6将此参数修改为了slow_query_log
slow_query_log_file="filename.log"long_query_time=10 //慢查的时长单位为秒,可以精确到小数点后6位(微秒)
保存 my.cnf 更改,重启 MySQL 服务。

关闭慢查询日志

设置 log_slow_queries 参数为 0:
# Slow logging.log-output=NONE
log_slow_queries=0 //MySQL 5.6将此参数修改为了slow_query_log
slow_query_log_file="filename.log"long_query_time=10
保存 my.cnf 更改,重启 MySQL 服务。

四 二进制日志

二进制日志记录 MySQL 数据库中所有与更新相关的操作,即二进制日志记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但是不包括数据查询语句。常用于恢复数据库和主从复制。

查看 log_bin 状态

mysql> SHOW VARIABLES LIKE 'log_bin%';
+---------------------------------+-------+
| Variable_name | Value |
+---------------------------------+-------+
| log_bin | OFF |
| log_bin_basename | |
| log_bin_index | |
| log_bin_trust_function_creators | OFF |
| log_bin_use_v1_row_events | OFF |
+---------------------------------+-------+

启用二进制日志功能

编辑 my.cnf ,在 [mysqld] 下添加
# Binary Logging.log-bin="filename-bin"
保存 my.cnf 更改,重启 MySQL 服务。

其他相关配置:

max_binlog_size={4096 .. 1073741824} ;
设定二进制日志文件上限,单位为字节,最小值为4K,最大值为1G,默认为1G。某事务所产生的日志信息只能写入一个二进制日志文件,因此,实际上的二进制日志文件可能大于这个指定的上限。作用范围为全局级别,可用于配置文件,属动态变量。

查看日志文件

在data目录下有一个mysql-bin.index便是索引文件,以mysql-bin开头并以数字结尾的文件为二进制日志文件。
查看所有的二进制文件:
mysql> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 276665 |
+------------------+-----------+
1 row in set (0.03 sec)
查看当前正在使用的二进制文件:
mysql> show master status;
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000003 | 107 | | |
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)

二进制日志滚动

当 MySQL 服务进程启动、当前二进制日志文件的大小已经超过上限时、执行 FLUSH LOG 时,MySQL 会创建一个新的二进制日志文件。新的编号大1的日志用于记录最新的日志,而原日志名字不会被改变。
手动滚动命令:flush logs;

查看日志详细

查看binlog日志有几种方式:
1. 使用show binlog events方式可以获取当前以及指定binlog的日志,
2. 使用mysqlbinlog命令行。

1. show binlog events方式

mysql> show binlog events; #默认会返回mysql-bin.000001的日志
#太多,我就不截屏了
mysql> show binlog events in 'mysql-bin.000002';
mysql> show binlog events in 'mysql-bin.000002' from 107;

2. mysqlbinlog命令行

mysqlbinlog在mysql/bin目录

I 查看binlog日志

[root@localhost bin]# ./mysqlbinlog ../data/mysql-bin.000003

II 查看binlog日志并输出

下面参考:使用mysqlbinlog提取二进制日志
c、提取指定position位置的binlog日志并输出到压缩文件
# mysqlbinlog --start-position="120" --stop-position="332" /opt/data/APP01bin.000001 |gzip >extra_01.sql.gz

d、提取指定position位置的binlog日志导入数据库
# mysqlbinlog --start-position="120" --stop-position="332" /opt/data/APP01bin.000001 | mysql -uroot -p

e、提取指定开始时间的binlog并输出到日志文件
# mysqlbinlog --start-datetime="2014-12-15 20:15:23" /opt/data/APP01bin.000002 --result-file=extra02.sql

f、提取指定位置的多个binlog日志文件
# mysqlbinlog --start-position="120" --stop-position="332" /opt/data/APP01bin.000001 /opt/data/APP01bin.000002|more

g、提取指定数据库binlog并转换字符集到UTF8
# mysqlbinlog --database=test --set-charset=utf8 /opt/data/APP01bin.000001 /opt/data/APP01bin.000002 >test.sql

h、远程提取日志,指定结束时间
# mysqlbinlog -urobin -p -h192.168.1.116 -P3306 --stop-datetime="2014-12-15 20:30:23" --read-from-remote-server mysql-bin.000033 |more

i、远程提取使用row格式的binlog日志并输出到本地文件
# mysqlbinlog -urobin -p -P3606 -h192.168.1.177 --read-from-remote-server -vv inst3606bin.000005 >row.sql

expire_logs_days 参数

在 my.cnf 中配置 expire_logs_days 参数指定二进制日志的有效天数,MySQL 会自动删除过期的二进制日志。expire_logs_days 设置在服务器启动或者 MySQL 切换二进制日志时生效,因此,如果二进制日志没有增长和切换,服务器不会清除老条目。
# 二进制日志的有效天数expire_logs_days = 5

清除二进制日志

mysql> RESET MASTER;
mysql> PURGE MASTER LOGS TO 'mysql-bin.000003';
mysql> PURGE MASTER LOGS BEFORE '2015-01-01 00:00:00';
mysql> PURGE MASTER LOGS BEFORE CURRENT_DATE - INTERVAL 10 DAY;
警告
由于二进制日志的重要性,请仅在确定不再需要将要被删除的二进制文件,或者在已经对二进制日志文件进行归档备份,或者已经进行数据库备份的情况下,才进行删除操作,且不要使用 rm 命令删除。

五 事务日志

出于性能和故障恢复的考虑,MySQL 服务器不会立即执行事务,而是先将事务记录在日志里面,这样可以将随机IO转换成顺序IO,从而提高IO性能。
事物日志通常是一组至少两个固定大小的文件,当其中一个写满时,MySQL会将事务日志写入另一个日志文件(先清空原有内容)。当 MySQL 从崩溃中恢复时,会读取事务日志,将其中已经 commit 的事务写入数据库,没有 commit 的事务 rollback 。
事务日志由存储引擎(innodb)管理,一般不需要手动干预。

六 中继日志

中继日志用于主从复制架构中的从服务器上,从服务器的 slave 进程从主服务器处获取二进制日志的内容并写入中继日志,然后由 IO 进程读取并执行中继日志中的语句。

日志分析工具




一.在线程中执行任务
任务执行的两个关键
  • 清晰的任务边界
  • 明确的任务执行策略
1.串行地执行任务
   在服务器应用程序中,穿行处理机制通常都无法提供高吞吐率和快速响应性(除非任务数量很少;只为单用户提供服务)
   但是串行处理方式能够带来简单些或安全性,因此在客户端很实用,大多数GUI框架都通过单一的线程来串行的处理任务。
2.显示的为任务创建线程
   在正常负载的情况下,“ 为每个任务”分配一个线程的方法能提升串行执行的性能。只要请求速率不超出服务器的请求处理能力,那么这种方法就可以同时带来更快的响应性和更高的吞吐率。
3.无限制创建线程的不足
  缺点如下:
  (1)线程生命周期的开销非常高。(创建和销毁开销)
  (2)资源消耗(内存、cpu、上下文切换)
  (3)稳定性(jvm和操作系统平台对可创建线程数量有限制,当然对于那些使用用户线程的语言,则没有限制)

 服务器应用绝对不要直接创建和使用线程!

二.Executor框架
串行执行的问题在于其槽糕的响应性和吞吐量,而为每个任务分配一个问题在于资源管理的复杂性。
在java类库当中,任务执行的主要抽象不是Thread,而是Executor.
Executor基于生产者-消费者模式,提交任务的操作相当于生产者(生成待完成的工作单元),执行任务的线程相当于消费者(执行完这些工作单元)。如果要在应用程序中实现一个生产者-消费者的设计,那最简单的方式就是通过Executor.
1.示例 基于Executor的web服务器
  Exector框架解耦了三个东西:任务-任务提交-任务执行策略
2.执行策略
3.线程池
线程池是指管理一组同构工作线程的资源池。线程池是与工作队列密切相关的,其中在工作队列中保存了所有等待执行的任务,工作者线程从工作队列中获取任务,执行任务,然后返回线程池并等待下一个任务。
客户端线程是生产者,工作者线程是消费者,工作队列是纽带。
在线程池中执行任务和一个任务一个线程相比的优势:
(1)通过重用现有的线程,而不是创建新的线程,可以处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。
(2)当任务请求到达时,工作者线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。
(3)适当的调节线程池的大小,可以创建出足够多的线程以便使处理器保持忙碌状态,同时还可以防止多线程相互竞争资源而使应用程序耗尽资源或失败。
推荐直接使用ThreadPoolExecutor来使用线程池。
注意:单线程Executor提供了大量的内部同步机制,从而确保了任务执行的内存写入操作的可见性。
4.Executor的生命周期
为了解决执行服务的生命周期的问题,ExecutorService扩展了Executor接口,添加了一些用于生命周期管理的方法(同时还有一些用于任务提交的便利方法)。

ExecutorService的生命周期有三种状态:运行、关闭和已终止。
任务处于三种状态:未提交、在任务队列中等待执行、正在运行和已完成。更官方的说法是创建、提交、开始、完成。
这儿翻译有问题,应该是先shutdown,然后awaitTermination
5.延迟任务与周期任务
Timer的缺陷:
  • Timer基于绝对时间,而不是相对时间
  • Timer是单线程模型,因此可能存在丢失任务或者连续执行任务的情况
  • Timer内部的逻辑一旦抛出异常,将彻底终止线程,新的任务也不会被调度
在java1.5之后应该考虑使用ScheduledExecutorService替代Timer.


三.找出可利用的并行性
核心和难点在于找出任务边界!只有当大量相互独立且同构的任务可以并发处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。


Future超时控制
ExecutorService.invokeAll&invokeAny方法








  

JAVA8源码如下:
/**
 * A collection of methods for performing low-level, unsafe operations.
 * Although the class and all methods are public, use of this class is
 * limited because only trusted code can obtain instances of it.
 *
 * @author John R. Rose
 * @see #getUnsafe
 */
public final class Unsafe {
    private static native void registerNatives();
    static {
        registerNatives();
        sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
    }
    private Unsafe() {}
    private static final Unsafe theUnsafe = new Unsafe();
    /**
     * Provides the caller with the capability of performing unsafe
     * operations.
     *
     * <p> The returned <code>Unsafe</code> object should be carefully guarded
     * by the caller, since it can be used to read and write data at arbitrary
     * memory addresses.  It must never be passed to untrusted code.
     *
     * <p> Most methods in this class are very low-level, and correspond to a
     * small number of hardware instructions (on typical machines).  Compilers
     * are encouraged to optimize these methods accordingly.
     *
     * <p> Here is a suggested idiom for using unsafe operations:
     *
     * <blockquote><pre>
     * class MyTrustedClass {
     *   private static final Unsafe unsafe = Unsafe.getUnsafe();
     *   ...
     *   private long myCountAddress = ...;
     *   public int getCount() { return unsafe.getByte(myCountAddress); }
     * }
     * </pre></blockquote>
     *
     * (It may assist compilers to make the local variable be
     * <code>final</code>.)
     *
     * @exception  SecurityException  if a security manager exists and its
     *             <code>checkPropertiesAccess</code> method doesn't allow
     *             access to the system properties.
     */
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class cc = Reflection.getCallerClass();
        if (cc.getClassLoader() != null)
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }
    /// peek and poke operations
    /// (compilers should optimize these to memory ops)
    // These work on object fields in the Java heap.
    // They will not work on elements of packed arrays.
    /**
     * Fetches a value from a given Java variable.
     * More specifically, fetches a field or array element within the given
     * object <code>o</code> at the given offset, or (if <code>o</code> is
     * null) from the memory address whose numerical value is the given
     * offset.
     * <p>
     * The results are undefined unless one of the following cases is true:
     * <ul>
     * <li>The offset was obtained from {@link #objectFieldOffset} on
     * the {@link java.lang.reflect.Field} of some Java field and the object
     * referred to by <code>o</code> is of a class compatible with that
     * field's class.
     *
     * <li>The offset and object reference <code>o</code> (either null or
     * non-null) were both obtained via {@link #staticFieldOffset}
     * and {@link #staticFieldBase} (respectively) from the
     * reflective {@link Field} representation of some Java field.
     *
     * <li>The object referred to by <code>o</code> is an array, and the offset
     * is an integer of the form <code>B+N*S</code>, where <code>N</code> is
     * a valid index into the array, and <code>B</code> and <code>S</code> are
     * the values obtained by {@link #arrayBaseOffset} and {@link
     * #arrayIndexScale} (respectively) from the array's class.  The value
     * referred to is the <code>N</code><em>th</em> element of the array.
     *
     * </ul>
     * <p>
     * If one of the above cases is true, the call references a specific Java
     * variable (field or array element).  However, the results are undefined
     * if that variable is not in fact of the type returned by this method.
     * <p>
     * This method refers to a variable by means of two parameters, and so
     * it provides (in effect) a <em>double-register</em> addressing mode
     * for Java variables.  When the object reference is null, this method
     * uses its offset as an absolute address.  This is similar in operation
     * to methods such as {@link #getInt(long)}, which provide (in effect) a
     * <em>single-register</em> addressing mode for non-Java variables.
     * However, because Java variables may have a different layout in memory
     * from non-Java variables, programmers should not assume that these
     * two addressing modes are ever equivalent.  Also, programmers should
     * remember that offsets from the double-register addressing mode cannot
     * be portably confused with longs used in the single-register addressing
     * mode.
     *
     * @param o Java heap object in which the variable resides, if any, else
     *        null
     * @param offset indication of where the variable resides in a Java heap
     *        object, if any, else a memory address locating the variable
     *        statically
     * @return the value fetched from the indicated Java variable
     * @throws RuntimeException No defined exceptions are thrown, not even
     *         {@link NullPointerException}
     */
    public native int getInt(Object o, long offset);
    /**
     * Stores a value into a given Java variable.
     * <p>
     * The first two parameters are interpreted exactly as with
     * {@link #getInt(Object, long)} to refer to a specific
     * Java variable (field or array element).  The given value
     * is stored into that variable.
     * <p>
     * The variable must be of the same type as the method
     * parameter <code>x</code>.
     *
     * @param o Java heap object in which the variable resides, if any, else
     *        null
     * @param offset indication of where the variable resides in a Java heap
     *        object, if any, else a memory address locating the variable
     *        statically
     * @param x the value to store into the indicated Java variable
     * @throws RuntimeException No defined exceptions are thrown, not even
     *         {@link NullPointerException}
     */
    public native void putInt(Object o, long offset, int x);
    /**
     * Fetches a reference value from a given Java variable.
     * @see #getInt(Object, long)
     */
    public native Object getObject(Object o, long offset);
    /**
     * Stores a reference value into a given Java variable.
     * <p>
     * Unless the reference <code>x</code> being stored is either null
     * or matches the field type, the results are undefined.
     * If the reference <code>o</code> is non-null, car marks or
     * other store barriers for that object (if the VM requires them)
     * are updated.
     * @see #putInt(Object, int, int)
     */
    public native void putObject(Object o, long offset, Object x);
    /** @see #getInt(Object, long) */
    public native boolean getBoolean(Object o, long offset);
    /** @see #putInt(Object, int, int) */
    public native void    putBoolean(Object o, long offset, boolean x);
    /** @see #getInt(Object, long) */
    public native byte    getByte(Object o, long offset);
    /** @see #putInt(Object, int, int) */
    public native void    putByte(Object o, long offset, byte x);
    /** @see #getInt(Object, long) */
    public native short   getShort(Object o, long offset);
    /** @see #putInt(Object, int, int) */
    public native void    putShort(Object o, long offset, short x);
    /** @see #getInt(Object, long) */
    public native char    getChar(Object o, long offset);
    /** @see #putInt(Object, int, int) */
    public native void    putChar(Object o, long offset, char x);
    /** @see #getInt(Object, long) */
    public native long    getLong(Object o, long offset);
    /** @see #putInt(Object, int, int) */
    public native void    putLong(Object o, long offset, long x);
    /** @see #getInt(Object, long) */
    public native float   getFloat(Object o, long offset);
    /** @see #putInt(Object, int, int) */
    public native void    putFloat(Object o, long offset, float x);
    /** @see #getInt(Object, long) */
    public native double  getDouble(Object o, long offset);
    /** @see #putInt(Object, int, int) */
    public native void    putDouble(Object o, long offset, double x);
    /**
     * This method, like all others with 32-bit offsets, was native
     * in a previous release but is now a wrapper which simply casts
     * the offset to a long value.  It provides backward compatibility
     * with bytecodes compiled against 1.4.
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public int getInt(Object o, int offset) {
        return getInt(o, (long)offset);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public void putInt(Object o, int offset, int x) {
        putInt(o, (long)offset, x);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public Object getObject(Object o, int offset) {
        return getObject(o, (long)offset);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public void putObject(Object o, int offset, Object x) {
        putObject(o, (long)offset, x);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public boolean getBoolean(Object o, int offset) {
        return getBoolean(o, (long)offset);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public void putBoolean(Object o, int offset, boolean x) {
        putBoolean(o, (long)offset, x);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public byte getByte(Object o, int offset) {
        return getByte(o, (long)offset);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public void putByte(Object o, int offset, byte x) {
        putByte(o, (long)offset, x);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public short getShort(Object o, int offset) {
        return getShort(o, (long)offset);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public void putShort(Object o, int offset, short x) {
        putShort(o, (long)offset, x);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public char getChar(Object o, int offset) {
        return getChar(o, (long)offset);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public void putChar(Object o, int offset, char x) {
        putChar(o, (long)offset, x);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public long getLong(Object o, int offset) {
        return getLong(o, (long)offset);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public void putLong(Object o, int offset, long x) {
        putLong(o, (long)offset, x);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public float getFloat(Object o, int offset) {
        return getFloat(o, (long)offset);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public void putFloat(Object o, int offset, float x) {
        putFloat(o, (long)offset, x);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public double getDouble(Object o, int offset) {
        return getDouble(o, (long)offset);
    }
    /**
     * @deprecated As of 1.4.1, cast the 32-bit offset argument to a long.
     * See {@link #staticFieldOffset}.
     */
    @Deprecated
    public void putDouble(Object o, int offset, double x) {
        putDouble(o, (long)offset, x);
    }
    // These work on values in the C heap.
    /**
     * Fetches a value from a given memory address.  If the address is zero, or
     * does not point into a block obtained from {@link #allocateMemory}, the
     * results are undefined.
     *
     * @see #allocateMemory
     */
    public native byte    getByte(long address);
    /**
     * Stores a value into a given memory address.  If the address is zero, or
     * does not point into a block obtained from {@link #allocateMemory}, the
     * results are undefined.
     *
     * @see #getByte(long)
     */
    public native void    putByte(long address, byte x);
    /** @see #getByte(long) */
    public native short   getShort(long address);
    /** @see #putByte(long, byte) */
    public native void    putShort(long address, short x);
    /** @see #getByte(long) */
    public native char    getChar(long address);
    /** @see #putByte(long, byte) */
    public native void    putChar(long address, char x);
    /** @see #getByte(long) */
    public native int     getInt(long address);
    /** @see #putByte(long, byte) */
    public native void    putInt(long address, int x);
    /** @see #getByte(long) */
    public native long    getLong(long address);
    /** @see #putByte(long, byte) */
    public native void    putLong(long address, long x);
    /** @see #getByte(long) */
    public native float   getFloat(long address);
    /** @see #putByte(long, byte) */
    public native void    putFloat(long address, float x);
    /** @see #getByte(long) */
    public native double  getDouble(long address);
    /** @see #putByte(long, byte) */
    public native void    putDouble(long address, double x);
    /**
     * Fetches a native pointer from a given memory address.  If the address is
     * zero, or does not point into a block obtained from {@link
     * #allocateMemory}, the results are undefined.
     *
     * <p> If the native pointer is less than 64 bits wide, it is extended as
     * an unsigned number to a Java long.  The pointer may be indexed by any
     * given byte offset, simply by adding that offset (as a simple integer) to
     * the long representing the pointer.  The number of bytes actually read
     * from the target address maybe determined by consulting {@link
     * #addressSize}.
     *
     * @see #allocateMemory
     */
    public native long getAddress(long address);
    /**
     * Stores a native pointer into a given memory address.  If the address is
     * zero, or does not point into a block obtained from {@link
     * #allocateMemory}, the results are undefined.
     *
     * <p> The number of bytes actually written at the target address maybe
     * determined by consulting {@link #addressSize}.
     *
     * @see #getAddress(long)
     */
    public native void putAddress(long address, long x);
    /// wrappers for malloc, realloc, free:
    /**
     * Allocates a new block of native memory, of the given size in bytes.  The
     * contents of the memory are uninitialized; they will generally be
     * garbage.  The resulting native pointer will never be zero, and will be
     * aligned for all value types.  Dispose of this memory by calling {@link
     * #freeMemory}, or resize it with {@link #reallocateMemory}.
     *
     * @throws IllegalArgumentException if the size is negative or too large
     *         for the native size_t type
     *
     * @throws OutOfMemoryError if the allocation is refused by the system
     *
     * @see #getByte(long)
     * @see #putByte(long, byte)
     */
    public native long allocateMemory(long bytes);
    /**
     * Resizes a new block of native memory, to the given size in bytes.  The
     * contents of the new block past the size of the old block are
     * uninitialized; they will generally be garbage.  The resulting native
     * pointer will be zero if and only if the requested size is zero.  The
     * resulting native pointer will be aligned for all value types.  Dispose
     * of this memory by calling {@link #freeMemory}, or resize it with {@link
     * #reallocateMemory}.  The address passed to this method may be null, in
     * which case an allocation will be performed.
     *
     * @throws IllegalArgumentException if the size is negative or too large
     *         for the native size_t type
     *
     * @throws OutOfMemoryError if the allocation is refused by the system
     *
     * @see #allocateMemory
     */
    public native long reallocateMemory(long address, long bytes);
    /**
     * Sets all bytes in a given block of memory to a fixed value
     * (usually zero).
     *
     * <p>This method determines a block's base address by means of two parameters,
     * and so it provides (in effect) a <em>double-register</em> addressing mode,
     * as discussed in {@link #getInt(Object,long)}.  When the object reference is null,
     * the offset supplies an absolute base address.
     *
     * <p>The stores are in coherent (atomic) units of a size determined
     * by the address and length parameters.  If the effective address and
     * length are all even modulo 8, the stores take place in 'long' units.
     * If the effective address and length are (resp.) even modulo 4 or 2,
     * the stores take place in units of 'int' or 'short'.
     *
     * @since 1.7
     */
    public native void setMemory(Object o, long offset, long bytes, byte value);
    /**
     * Sets all bytes in a given block of memory to a fixed value
     * (usually zero).  This provides a <em>single-register</em> addressing mode,
     * as discussed in {@link #getInt(Object,long)}.
     *
     * <p>Equivalent to <code>setMemory(null, address, bytes, value)</code>.
     */
    public void setMemory(long address, long bytes, byte value) {
        setMemory(null, address, bytes, value);
    }
    /**
     * Sets all bytes in a given block of memory to a copy of another
     * block.
     *
     * <p>This method determines each block's base address by means of two parameters,
     * and so it provides (in effect) a <em>double-register</em> addressing mode,
     * as discussed in {@link #getInt(Object,long)}.  When the object reference is null,
     * the offset supplies an absolute base address.
     *
     * <p>The transfers are in coherent (atomic) units of a size determined
     * by the address and length parameters.  If the effective addresses and
     * length are all even modulo 8, the transfer takes place in 'long' units.
     * If the effective addresses and length are (resp.) even modulo 4 or 2,
     * the transfer takes place in units of 'int' or 'short'.
     *
     * @since 1.7
     */
    public native void copyMemory(Object srcBase, long srcOffset,
                                  Object destBase, long destOffset,
                                  long bytes);
    /**
     * Sets all bytes in a given block of memory to a copy of another
     * block.  This provides a <em>single-register</em> addressing mode,
     * as discussed in {@link #getInt(Object,long)}.
     *
     * Equivalent to <code>copyMemory(null, srcAddress, null, destAddress, bytes)</code>.
     */
    public void copyMemory(long srcAddress, long destAddress, long bytes) {
        copyMemory(null, srcAddress, null, destAddress, bytes);
    }
    /**
     * Disposes of a block of native memory, as obtained from {@link
     * #allocateMemory} or {@link #reallocateMemory}.  The address passed to
     * this method may be null, in which case no action is taken.
     *
     * @see #allocateMemory
     */
    public native void freeMemory(long address);
    /// random queries
    /**
     * This constant differs from all results that will ever be returned from
     * {@link #staticFieldOffset}, {@link #objectFieldOffset},
     * or {@link #arrayBaseOffset}.
     */
    public static final int INVALID_FIELD_OFFSET   = -1;
    /**
     * Returns the offset of a field, truncated to 32 bits.
     * This method is implemented as follows:
     * <blockquote><pre>
     * public int fieldOffset(Field f) {
     *     if (Modifier.isStatic(f.getModifiers()))
     *         return (int) staticFieldOffset(f);
     *     else
     *         return (int) objectFieldOffset(f);
     * }
     * </pre></blockquote>
     * @deprecated As of 1.4.1, use {@link #staticFieldOffset} for static
     * fields and {@link #objectFieldOffset} for non-static fields.
     */
    @Deprecated
    public int fieldOffset(Field f) {
        if (Modifier.isStatic(f.getModifiers()))
            return (int) staticFieldOffset(f);
        else
            return (int) objectFieldOffset(f);
    }
    /**
     * Returns the base address for accessing some static field
     * in the given class.  This method is implemented as follows:
     * <blockquote><pre>
     * public Object staticFieldBase(Class c) {
     *     Field[] fields = c.getDeclaredFields();
     *     for (int i = 0; i < fields.length; i++) {
     *         if (Modifier.isStatic(fields[i].getModifiers())) {
     *             return staticFieldBase(fields[i]);
     *         }
     *     }
     *     return null;
     * }
     * </pre></blockquote>
     * @deprecated As of 1.4.1, use {@link #staticFieldBase(Field)}
     * to obtain the base pertaining to a specific {@link Field}.
     * This method works only for JVMs which store all statics
     * for a given class in one place.
     */
    @Deprecated
    public Object staticFieldBase(Class c) {
        Field[] fields = c.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            if (Modifier.isStatic(fields[i].getModifiers())) {
                return staticFieldBase(fields[i]);
            }
        }
        return null;
    }
    /**
     * Report the location of a given field in the storage allocation of its
     * class.  Do not expect to perform any sort of arithmetic on this offset;
     * it is just a cookie which is passed to the unsafe heap memory accessors.
     *
     * <p>Any given field will always have the same offset and base, and no
     * two distinct fields of the same class will ever have the same offset
     * and base.
     *
     * <p>As of 1.4.1, offsets for fields are represented as long values,
     * although the Sun JVM does not use the most significant 32 bits.
     * However, JVM implementations which store static fields at absolute
     * addresses can use long offsets and null base pointers to express
     * the field locations in a form usable by {@link #getInt(Object,long)}.
     * Therefore, code which will be ported to such JVMs on 64-bit platforms
     * must preserve all bits of static field offsets.
     * @see #getInt(Object, long)
     */
    public native long staticFieldOffset(Field f);
    /**
     * Report the location of a given static field, in conjunction with {@link
     * #staticFieldBase}.
     * <p>Do not expect to perform any sort of arithmetic on this offset;
     * it is just a cookie which is passed to the unsafe heap memory accessors.
     *
     * <p>Any given field will always have the same offset, and no two distinct
     * fields of the same class will ever have the same offset.
     *
     * <p>As of 1.4.1, offsets for fields are represented as long values,
     * although the Sun JVM does not use the most significant 32 bits.
     * It is hard to imagine a JVM technology which needs more than
     * a few bits to encode an offset within a non-array object,
     * However, for consistency with other methods in this class,
     * this method reports its result as a long value.
     * @see #getInt(Object, long)
     */
    public native long objectFieldOffset(Field f);
    /**
     * Report the location of a given static field, in conjunction with {@link
     * #staticFieldOffset}.
     * <p>Fetch the base "Object", if any, with which static fields of the
     * given class can be accessed via methods like {@link #getInt(Object,
     * long)}.  This value may be null.  This value may refer to an object
     * which is a "cookie", not guaranteed to be a real Object, and it should
     * not be used in any way except as argument to the get and put routines in
     * this class.
     */
    public native Object staticFieldBase(Field f);
    /**
     * Detect if the given class may need to be initialized. This is often
     * needed in conjunction with obtaining the static field base of a
     * class.
     * @return false only if a call to {@code ensureClassInitialized} would have no effect
     */
    public native boolean shouldBeInitialized(Class<?> c);
    /**
     * Ensure the given class has been initialized. This is often
     * needed in conjunction with obtaining the static field base of a
     * class.
     */
    public native void ensureClassInitialized(Class c);
    /**
     * Report the offset of the first element in the storage allocation of a
     * given array class.  If {@link #arrayIndexScale} returns a non-zero value
     * for the same class, you may use that scale factor, together with this
     * base offset, to form new offsets to access elements of arrays of the
     * given class.
     *
     * @see #getInt(Object, long)
     * @see #putInt(Object, long, int)
     */
    public native int arrayBaseOffset(Class arrayClass);
    /** The value of {@code arrayBaseOffset(boolean[].class)} */
    public static final int ARRAY_BOOLEAN_BASE_OFFSET
            = theUnsafe.arrayBaseOffset(boolean[].class);
    /** The value of {@code arrayBaseOffset(byte[].class)} */
    public static final int ARRAY_BYTE_BASE_OFFSET
            = theUnsafe.arrayBaseOffset(byte[].class);
    /** The value of {@code arrayBaseOffset(short[].class)} */
    public static final int ARRAY_SHORT_BASE_OFFSET
            = theUnsafe.arrayBaseOffset(short[].class);
    /** The value of {@code arrayBaseOffset(char[].class)} */
    public static final int ARRAY_CHAR_BASE_OFFSET
            = theUnsafe.arrayBaseOffset(char[].class);
    /** The value of {@code arrayBaseOffset(int[].class)} */
    public static final int ARRAY_INT_BASE_OFFSET
            = theUnsafe.arrayBaseOffset(int[].class);
    /** The value of {@code arrayBaseOffset(long[].class)} */
    public static final int ARRAY_LONG_BASE_OFFSET
            = theUnsafe.arrayBaseOffset(long[].class);
    /** The value of {@code arrayBaseOffset(float[].class)} */
    public static final int ARRAY_FLOAT_BASE_OFFSET
            = theUnsafe.arrayBaseOffset(float[].class);
    /** The value of {@code arrayBaseOffset(double[].class)} */
    public static final int ARRAY_DOUBLE_BASE_OFFSET
            = theUnsafe.arrayBaseOffset(double[].class);
    /** The value of {@code arrayBaseOffset(Object[].class)} */
    public static final int ARRAY_OBJECT_BASE_OFFSET
            = theUnsafe.arrayBaseOffset(Object[].class);
    /**
     * Report the scale factor for addressing elements in the storage
     * allocation of a given array class.  However, arrays of "narrow" types
     * will generally not work properly with accessors like {@link
     * #getByte(Object, int)}, so the scale factor for such classes is reported
     * as zero.
     *
     * @see #arrayBaseOffset
     * @see #getInt(Object, long)
     * @see #putInt(Object, long, int)
     */
    public native int arrayIndexScale(Class arrayClass);
    /** The value of {@code arrayIndexScale(boolean[].class)} */
    public static final int ARRAY_BOOLEAN_INDEX_SCALE
            = theUnsafe.arrayIndexScale(boolean[].class);
    /** The value of {@code arrayIndexScale(byte[].class)} */
    public static final int ARRAY_BYTE_INDEX_SCALE
            = theUnsafe.arrayIndexScale(byte[].class);
    /** The value of {@code arrayIndexScale(short[].class)} */
    public static final int ARRAY_SHORT_INDEX_SCALE
            = theUnsafe.arrayIndexScale(short[].class);
    /** The value of {@code arrayIndexScale(char[].class)} */
    public static final int ARRAY_CHAR_INDEX_SCALE
            = theUnsafe.arrayIndexScale(char[].class);
    /** The value of {@code arrayIndexScale(int[].class)} */
    public static final int ARRAY_INT_INDEX_SCALE
            = theUnsafe.arrayIndexScale(int[].class);
    /** The value of {@code arrayIndexScale(long[].class)} */
    public static final int ARRAY_LONG_INDEX_SCALE
            = theUnsafe.arrayIndexScale(long[].class);
    /** The value of {@code arrayIndexScale(float[].class)} */
    public static final int ARRAY_FLOAT_INDEX_SCALE
            = theUnsafe.arrayIndexScale(float[].class);
    /** The value of {@code arrayIndexScale(double[].class)} */
    public static final int ARRAY_DOUBLE_INDEX_SCALE
            = theUnsafe.arrayIndexScale(double[].class);
    /** The value of {@code arrayIndexScale(Object[].class)} */
    public static final int ARRAY_OBJECT_INDEX_SCALE
            = theUnsafe.arrayIndexScale(Object[].class);
    /**
     * Report the size in bytes of a native pointer, as stored via {@link
     * #putAddress}.  This value will be either 4 or 8.  Note that the sizes of
     * other primitive types (as stored in native memory blocks) is determined
     * fully by their information content.
     */
    public native int addressSize();
    /** The value of {@code addressSize()} */
    public static final int ADDRESS_SIZE = theUnsafe.addressSize();
    /**
     * Report the size in bytes of a native memory page (whatever that is).
     * This value will always be a power of two.
     */
    public native int pageSize();
    /// random trusted operations from JNI:
    /**
     * Tell the VM to define a class, without security checks.  By default, the
     * class loader and protection domain come from the caller's class.
     */
    public native Class defineClass(String name, byte[] b, int off, int len,
                                    ClassLoader loader,
                                    ProtectionDomain protectionDomain);
    /**
     * @deprecated Use defineClass(String, byte[], int, int, ClassLoader, ProtectionDomain)
     *             instead. This method will be removed in JDK 8.
     */
    @Deprecated
    @CallerSensitive
    public native Class defineClass(String name, byte[] b, int off, int len);
    /**
     * Define a class but do not make it known to the class loader or system dictionary.
     * <p>
     * For each CP entry, the corresponding CP patch must either be null or have
     * the a format that matches its tag:
     * <ul>
     * <li>Integer, Long, Float, Double: the corresponding wrapper object type from java.lang
     * <li>Utf8: a string (must have suitable syntax if used as signature or name)
     * <li>Class: any java.lang.Class object
     * <li>String: any object (not just a java.lang.String)
     * <li>InterfaceMethodRef: (NYI) a method handle to invoke on that call site's arguments
     * </ul>
     * @params hostClass context for linkage, access control, protection domain, and class loader
     * @params data      bytes of a class file
     * @params cpPatches where non-null entries exist, they replace corresponding CP entries in data
     */
    public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);
    /** Allocate an instance but do not run any constructor.
        Initializes the class if it has not yet been. */
    public native Object allocateInstance(Class cls)
        throws InstantiationException;
    /** Lock the object.  It must get unlocked via {@link #monitorExit}. */
    public native void monitorEnter(Object o);
    /**
     * Unlock the object.  It must have been locked via {@link
     * #monitorEnter}.
     */
    public native void monitorExit(Object o);
    /**
     * Tries to lock the object.  Returns true or false to indicate
     * whether the lock succeeded.  If it did, the object must be
     * unlocked via {@link #monitorExit}.
     */
    public native boolean tryMonitorEnter(Object o);
    /** Throw the exception without telling the verifier. */
    public native void throwException(Throwable ee);
    /**
     * Atomically update Java variable to <tt>x</tt> if it is currently
     * holding <tt>expected</tt>.
     * @return <tt>true</tt> if successful
     */
    public final native boolean compareAndSwapObject(Object o, long offset,
                                                     Object expected,
                                                     Object x);
    /**
     * Atomically update Java variable to <tt>x</tt> if it is currently
     * holding <tt>expected</tt>.
     * @return <tt>true</tt> if successful
     */
    public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);
    /**
     * Atomically update Java variable to <tt>x</tt> if it is currently
     * holding <tt>expected</tt>.
     * @return <tt>true</tt> if successful
     */
    public final native boolean compareAndSwapLong(Object o, long offset,
                                                   long expected,
                                                   long x);
    /**
     * Fetches a reference value from a given Java variable, with volatile
     * load semantics. Otherwise identical to {@link #getObject(Object, long)}
     */
    public native Object getObjectVolatile(Object o, long offset);
    /**
     * Stores a reference value into a given Java variable, with
     * volatile store semantics. Otherwise identical to {@link #putObject(Object, long, Object)}
     */
    public native void    putObjectVolatile(Object o, long offset, Object x);
    /** Volatile version of {@link #getInt(Object, long)}  */
    public native int     getIntVolatile(Object o, long offset);
    /** Volatile version of {@link #putInt(Object, long, int)}  */
    public native void    putIntVolatile(Object o, long offset, int x);
    /** Volatile version of {@link #getBoolean(Object, long)}  */
    public native boolean getBooleanVolatile(Object o, long offset);
    /** Volatile version of {@link #putBoolean(Object, long, boolean)}  */
    public native void    putBooleanVolatile(Object o, long offset, boolean x);
    /** Volatile version of {@link #getByte(Object, long)}  */
    public native byte    getByteVolatile(Object o, long offset);
    /** Volatile version of {@link #putByte(Object, long, byte)}  */
    public native void    putByteVolatile(Object o, long offset, byte x);
    /** Volatile version of {@link #getShort(Object, long)}  */
    public native short   getShortVolatile(Object o, long offset);
    /** Volatile version of {@link #putShort(Object, long, short)}  */
    public native void    putShortVolatile(Object o, long offset, short x);
    /** Volatile version of {@link #getChar(Object, long)}  */
    public native char    getCharVolatile(Object o, long offset);
    /** Volatile version of {@link #putChar(Object, long, char)}  */
    public native void    putCharVolatile(Object o, long offset, char x);
    /** Volatile version of {@link #getLong(Object, long)}  */
    public native long    getLongVolatile(Object o, long offset);
    /** Volatile version of {@link #putLong(Object, long, long)}  */
    public native void    putLongVolatile(Object o, long offset, long x);
    /** Volatile version of {@link #getFloat(Object, long)}  */
    public native float   getFloatVolatile(Object o, long offset);
    /** Volatile version of {@link #putFloat(Object, long, float)}  */
    public native void    putFloatVolatile(Object o, long offset, float x);
    /** Volatile version of {@link #getDouble(Object, long)}  */
    public native double  getDoubleVolatile(Object o, long offset);
    /** Volatile version of {@link #putDouble(Object, long, double)}  */
    public native void    putDoubleVolatile(Object o, long offset, double x);
    /**
     * Version of {@link #putObjectVolatile(Object, long, Object)}
     * that does not guarantee immediate visibility of the store to
     * other threads. This method is generally only useful if the
     * underlying field is a Java volatile (or if an array cell, one
     * that is otherwise only accessed using volatile accesses).
     */
    public native void    putOrderedObject(Object o, long offset, Object x);
    /** Ordered/Lazy version of {@link #putIntVolatile(Object, long, int)}  */
    public native void    putOrderedInt(Object o, long offset, int x);
    /** Ordered/Lazy version of {@link #putLongVolatile(Object, long, long)} */
    public native void    putOrderedLong(Object o, long offset, long x);
    /**
     * Unblock the given thread blocked on <tt>park</tt>, or, if it is
     * not blocked, cause the subsequent call to <tt>park</tt> not to
     * block.  Note: this operation is "unsafe" solely because the
     * caller must somehow ensure that the thread has not been
     * destroyed. Nothing special is usually required to ensure this
     * when called from Java (in which there will ordinarily be a live
     * reference to the thread) but this is not nearly-automatically
     * so when calling from native code.
     * @param thread the thread to unpark.
     *
     */
    public native void unpark(Object thread);
    /**
     * Block current thread, returning when a balancing
     * <tt>unpark</tt> occurs, or a balancing <tt>unpark</tt> has
     * already occurred, or the thread is interrupted, or, if not
     * absolute and time is not zero, the given time nanoseconds have
     * elapsed, or if absolute, the given deadline in milliseconds
     * since Epoch has passed, or spuriously (i.e., returning for no
     * "reason"). Note: This operation is in the Unsafe class only
     * because <tt>unpark</tt> is, so it would be strange to place it
     * elsewhere.
     */
    public native void park(boolean isAbsolute, long time);
    /**
     * Gets the load average in the system run queue assigned
     * to the available processors averaged over various periods of time.
     * This method retrieves the given <tt>nelem</tt> samples and
     * assigns to the elements of the given <tt>loadavg</tt> array.
     * The system imposes a maximum of 3 samples, representing
     * averages over the last 1,  5,  and  15 minutes, respectively.
     *
     * @params loadavg an array of double of size nelems
     * @params nelems the number of samples to be retrieved and
     *         must be 1 to 3.
     *
     * @return the number of samples actually retrieved; or -1
     *         if the load average is unobtainable.
     */
    public native int getLoadAverage(double[] loadavg, int nelems);
}


摘要: Spring是一个开源框架,是为了解决企业应用程序开发复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许您选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架
写在前面
很多人在微信公众号中给我留言说想看spring的思维导图,正好也打算写。与其他框架相比,spring项目拥有更多的模块,我们常用的ioc,mvc,aop等,这些是spring的主要板块。一篇文章也不可能全部都讲,所以,我打算先把spring简介说一下,后续再写ioc,mvc和aop。
关于Spring
Spring是一个开源框架,是为了解决企业应用程序开发复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许您选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架。
它是一个全面的、企业应用开发一站式的解决方案,贯穿表现层、业务层、持久层。但是Spring仍然可以和其他的框架无缝整合。
Sping架构
Spring框架是分模块存在,除了最核心的Spring Core Container(即Spring容器)是必要模块之外,其他模块都是可选,视需要而定。大约有20多个模块。
Spring3与Spring4是有区别的,4.0主要是对Java 8的新函数式语法进行支持,还有加强了对网络各种新技术比如http-streaming, websocket的更好的支持。
一般来说,Spring主要分为7个模块:
Spring的主要jar包
常用注解
bean注入与装配的的方式有很多种,可以通过xml,getset方式,构造函数或者注解等。简单易用的方式就是使用Spring的注解了,Spring提供了大量的注解方式,让项目阅读和开发起来更加方便。
第三方框架集成
Spring框架的开发不是为了替代现有的优秀第三方框架,而是通过集成的方式把它们都连接起来。下面总结了一些常集成的优秀框架。
最后
这一节简单介绍了Spring,没涉及到原理的东西。Spring如此博大精深,希望大家好好学习哈。
原文地址:https://my.oschina.net/u/3080373/blog/891918


 Java面试通关要点汇总集

声明 原著作者-梁老师(梁桂钊)

基础篇

基本功

  • 面向对象的特征
  • final, finally, finalize 的区别
  • int 和 Integer 有什么区别
  • 重载和重写的区别
  • 抽象类和接口有什么区别
  • 说说反射的用途及实现
  • 说说自定义注解的场景及实现
  • HTTP 请求的 GET 与 POST 方式的区别
  • session 与 cookie 区别
  • session 分布式处理
  • JDBC 流程
  • MVC 设计思想
  • equals 与 == 的区别

集合

  • List 和 Set 区别
  • List 和 Map 区别
  • Arraylist 与 LinkedList 区别
  • ArrayList 与 Vector 区别
  • HashMap 和 Hashtable 的区别
  • HashSet 和 HashMap 区别
  • HashMap 和 ConcurrentHashMap 的区别
  • HashMap 的工作原理及代码实现
  • ConcurrentHashMap 的工作原理及代码实现

线程

  • 创建线程的方式及实现
  • sleep() 、join()、yield()有什么区别
  • 说说 CountDownLatch 原理
  • 说说 CyclicBarrier 原理
  • 说说 Semaphore 原理
  • 说说 Exchanger 原理
  • 说说 CountDownLatch 与 CyclicBarrier 区别
  • ThreadLocal 原理分析
  • 讲讲线程池的实现原理
  • 线程池的几种方式
  • 线程的生命周期

锁机制

  • 说说线程安全问题
  • volatile 实现原理
  • synchronize 实现原理
  • synchronized 与 lock 的区别
  • CAS 乐观锁
  • ABA 问题
  • 乐观锁的业务场景及实现方式

核心篇

数据存储

  • MySQL 索引使用的注意事项
  • 说说反模式设计
  • 说说分库与分表设计
  • 分库与分表带来的分布式困境与应对之策
  • 说说 SQL 优化之道
  • MySQL 遇到的死锁问题
  • 存储引擎的 InnoDB 与 MyISAM
  • 数据库索引的原理
  • 为什么要用 B-tree
  • 聚集索引与非聚集索引的区别
  • limit 20000 加载很慢怎么解决
  • 选择合适的分布式主键方案
  • 选择合适的数据存储方案
  • ObjectId 规则
  • 聊聊 MongoDB 使用场景
  • 倒排索引
  • 聊聊 ElasticSearch 使用场景

缓存使用

  • Redis 有哪些类型
  • Redis 内部结构
  • 聊聊 Redis 使用场景
  • Redis 持久化机制
  • Redis 如何实现持久化
  • Redis 集群方案与实现
  • Redis 为什么是单线程的
  • 缓存奔溃
  • 缓存降级
  • 使用缓存的合理性问题

消息队列

  • 消息队列的使用场景
  • 消息的重发补偿解决思路
  • 消息的幂等性解决思路
  • 消息的堆积解决思路
  • 自己如何实现消息队列
  • 如何保证消息的有序性

框架篇

Spring

  • BeanFactory 和 ApplicationContext 有什么区别
  • Spring Bean 的生命周期
  • Spring IOC 如何实现
  • 说说 Spring AOP
  • Spring AOP 实现原理
  • 动态代理(cglib 与 JDK)
  • Spring 事务实现方式
  • Spring 事务底层原理
  • 如何自定义注解实现功能
  • Spring MVC 运行流程
  • Spring MVC 启动流程
  • Spring 的单例实现原理
  • Spring 框架中用到了哪些设计模式
  • Spring 其他产品(Srping Boot、Spring Cloud、Spring Secuirity、Spring Data、Spring AMQP 等)

Netty

  • 为什么选择 Netty
  • 说说业务中,Netty 的使用场景
  • 原生的 NIO 在 JDK 1.7 版本存在 epoll bug
  • 什么是TCP 粘包/拆包
  • TCP粘包/拆包的解决办法
  • Netty 线程模型
  • 说说 Netty 的零拷贝
  • Netty 内部执行流程
  • Netty 重连实现

微服务篇

微服务

  • 前后端分离是如何做的
  • 微服务哪些框架
  • 你怎么理解 RPC 框架
  • 说说 RPC 的实现原理
  • 说说 Dubbo 的实现原理
  • 你怎么理解 RESTful
  • 说说如何设计一个良好的 API
  • 如何理解 RESTful API 的幂等性
  • 如何保证接口的幂等性
  • 说说 CAP 定理、 BASE 理论
  • 怎么考虑数据一致性问题
  • 说说最终一致性的实现方案
  • 你怎么看待微服务
  • 微服务与 SOA 的区别
  • 如何拆分服务
  • 微服务如何进行数据库管理
  • 如何应对微服务的链式调用异常
  • 对于快速追踪与定位问题
  • 微服务的安全

分布式

  • 谈谈业务中使用分布式的场景
  • Session 分布式方案
  • 分布式锁的场景
  • 分布是锁的实现方案
  • 分布式事务
  • 集群与负载均衡的算法与实现
  • 说说分库与分表设计
  • 分库与分表带来的分布式困境与应对之策

安全问题

  • 安全要素与 STRIDE 威胁
  • 防范常见的 Web 攻击
  • 服务端通信安全攻防
  • HTTPS 原理剖析
  • HTTPS 降级攻击
  • 授权与认证
  • 基于角色的访问控制
  • 基于数据的访问控制

性能优化

  • 性能指标有哪些
  • 如何发现性能瓶颈
  • 性能调优的常见手段
  • 说说你在项目中如何进行性能调优

工程篇

需求分析

  • 你如何对需求原型进行理解和拆分
  • 说说你对功能性需求的理解
  • 说说你对非功能性需求的理解
  • 你针对产品提出哪些交互和改进意见
  • 你如何理解用户痛点

设计能力

  • 说说你在项目中使用过的 UML 图
  • 你如何考虑组件化
  • 你如何考虑服务化
  • 你如何进行领域建模
  • 你如何划分领域边界
  • 说说你项目中的领域建模
  • 说说概要设计

设计模式

  • 你项目中有使用哪些设计模式
  • 说说常用开源框架中设计模式使用分析
  • 说说你对设计原则的理解
  • 23种设计模式的设计理念
  • 设计模式之间的异同,例如策略模式与状态模式的区别
  • 设计模式之间的结合,例如策略模式+简单工厂模式的实践
  • 设计模式的性能,例如单例模式哪种性能更好。

业务工程

  • 你系统中的前后端分离是如何做的
  • 说说你的开发流程
  • 你和团队是如何沟通的
  • 你如何进行代码评审
  • 说说你对技术与业务的理解
  • 说说你在项目中经常遇到的 Exception
  • 说说你在项目中遇到感觉最难Bug,怎么解决的
  • 说说你在项目中遇到印象最深困难,怎么解决的
  • 你觉得你们项目还有哪些不足的地方
  • 你是否遇到过 CPU 100% ,如何排查与解决
  • 你是否遇到过 内存 OOM ,如何排查与解决
  • 说说你对敏捷开发的实践
  • 说说你对开发运维的实践
  • 介绍下工作中的一个对自己最有价值的项目,以及在这个过程中的角色

软实力

  • 说说你的亮点
  • 说说你最近在看什么书
  • 说说你觉得最有意义的技术书籍
  • 工作之余做什么事情
  • 说说个人发展方向方面的思考
  • 说说你认为的服务端开发工程师应该具备哪些能力
  • 说说你认为的架构师是什么样的,架构师主要做什么
  • 说说你所理解的技术专家
原文地址:http://www.spring4all.com/article/716

原文地址:https://www.cnblogs.com/aspnethot/articles/1504082.html
官方说法:

聚集索引

  一种索引,该索引中键值的逻辑顺序决定了表中相应行的物理顺序。 
  聚集索引确定表中数据的物理顺序。聚集索引类似于电话簿,后者按姓氏排列数据。由于聚集索引规定数据在表中的物理存储顺序,因此一个表只能包含一个聚集索引。但该索引可以包含多个列(组合索引),就像电话簿按姓氏和名字进行组织一样。 
  聚集索引对于那些经常要搜索范围值的列特别有效。使用聚集索引找到包含第一个值的行后,便可以确保包含后续索引值的行在物理相邻。例如,如果应用程序执行 的一个查询经常检索某一日期范围内的记录,则使用聚集索引可以迅速找到包含开始日期的行,然后检索表中所有相邻的行,直到到达结束日期。这样有助于提高此 类查询的性能。同样,如果对从表中检索的数据进行排序时经常要用到某一列,则可以将该表在该列上聚集(物理排序),避免每次查询该列时都进行排序,从而节 省成本。 
      当索引值唯一时,使用聚集索引查找特定的行也很有效率。例如,使用唯一雇员 ID 列 emp_id 查找特定雇员的最快速的方法,是在 emp_id 列上创建聚集索引或 PRIMARY KEY 约束。

非聚集索引

  一种索引,该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同。
索引是通过二叉树的数据结构来描述的,我们可以这么理解聚簇索引:索引的叶节点就是数据节点。而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。如下图: 
 
                                      (非聚集索引)
 
                                      (聚集索引)
    一、深入浅出理解索引结构

      实际上,您可以把索引理解为一种特殊的目录。微软的SQL SERVER提供了两种索引:聚集索引(clustered index,也称聚类索引、簇集索引)和非聚集索引(nonclustered index,也称非聚类索引、非簇集索引)。下面,我们举例来说明一下聚集索引和非聚集索引的区别:
      其实,我们的汉语字典的正文本身就是一个聚集索引。比如,我们要查“安”字,就会很自然地翻开字典的前几页,因为“安”的拼音是“an”,而按照拼音排序汉字的字典是以英文字母“a”开头并以“z”结尾的,那么“安”字就自然地排在字典的前部。如果您翻完了所有以“a”开头的部分仍然找不到这个字,那么就说明您的字典中没有这个字;同样的,如果查“张”字,那您也会将您的字典翻到最后部分,因为“张”的拼音是“zhang”。也就是说,字典的正文部分本身就是一个目录,您不需要再去查其他目录来找到您需要找的内容。我们把这种正文内容本身就是一种按照一定规则排列的目录称为“聚集索引”。
      如果您认识某个字,您可以快速地从自动中查到这个字。但您也可能会遇到您不认识的字,不知道它的发音,这时候,您就不能按照刚才的方法找到您要查的字,而需要去根据“偏旁部首”查到您要找的字,然后根据这个字后的页码直接翻到某页来找到您要找的字。但您结合“部首目录”和“检字表”而查到的字的排序并不是真正的正文的排序方法,比如您查“张”字,我们可以看到在查部首之后的检字表中“张”的页码是672页,检字表中“张”的上面是“驰”字,但页码却是63页,“张”的下面是“弩”字,页面是390页。很显然,这些字并不是真正的分别位于“张”字的上下方,现在您看到的连续的“驰、张、弩”三字实际上就是他们在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我们可以通过这种方式来找到您所需要的字,但它需要两个过程,先找到目录中的结果,然后再翻到您所需要的页码。我们把这种目录纯粹是目录,正文纯粹是正文的排序方式称为“非聚集索引”。
      通过以上例子,我们可以理解到什么是“聚集索引”和“非聚集索引”。进一步引申一下,我们可以很容易的理解:每个表只能有一个聚集索引,因为目录只能按照一种方法进行排序。

    二、何时使用聚集索引或非聚集索引

下面的表总结了何时使用聚集索引或非聚集索引(很重要):
 
动作描述
使用聚集索引
使用非聚集索引
列经常被分组排序
返回某范围内的数据
不应
一个或极少不同值
不应
不应
小数目的不同值
不应
大数目的不同值
不应
频繁更新的列
不应
外键列
主键列
频繁修改索引列
不应


      事实上,我们可以通过前面聚集索引和非聚集索引的定义的例子来理解上表。如:返回某范围内的数据一项。比如您的某个表有一个时间列,恰好您把聚合索引建立在了该列,这时您查询2004年1月1日至2004年10月1日之间的全部数据时,这个速度就将是很快的,因为您的这本字典正文是按日期进行排序的,聚类索引只需要找到要检索的所有数据中的开头和结尾数据即可;而不像非聚集索引,必须先查到目录中查到每一项数据对应的页码,然后再根据页码查到具体内容。

    三、结合实际,谈索引使用的误区

      理论的目的是应用。虽然我们刚才列出了何时应使用聚集索引或非聚集索引,但在实践中以上规则却很容易被忽视或不能根据实际情况进行综合分析。下面我们将根据在实践中遇到的实际问题来谈一下索引使用的误区,以便于大家掌握索引建立的方法。

    1、主键就是聚集索引
      这种想法笔者认为是极端错误的,是对聚集索引的一种浪费。虽然SQL SERVER默认是在主键上建立聚集索引的。
      通常,我们会在每个表中都建立一个ID列,以区分每条数据,并且这个ID列是自动增大的,步长一般为1。我们的这个办公自动化的实例中的列Gid就是如此。此时,如果我们将这个列设为主键,SQL SERVER会将此列默认为聚集索引。这样做有好处,就是可以让您的数据在数据库中按照ID进行物理排序,但笔者认为这样做意义不大。
      显而易见,聚集索引的优势是很明显的,而每个表中只能有一个聚集索引的规则,这使得聚集索引变得更加珍贵。
      从我们前面谈到的聚集索引的定义我们可以看出,使用聚集索引的最大好处就是能够根据查询要求,迅速缩小查询范围,避免全表扫描。在实际应用中,因为 ID号是自动生成的,我们并不知道每条记录的ID号,所以我们很难在实践中用ID号来进行查询。这就使让ID号这个主键作为聚集索引成为一种资源浪费。其次,让每个ID号都不同的字段作为聚集索引也不符合“大数目的不同值情况下不应建立聚合索引”规则;当然,这种情况只是针对用户经常修改记录内容,特别是索引项的时候会负作用,但对于查询速度并没有影响。
      在办公自动化系统中,无论是系统首页显示的需要用户签收的文件、会议还是用户进行文件查询等任何情况下进行数据查询都离不开字段的是“日期”还有用户本身的“用户名”。
      通常,办公自动化的首页会显示每个用户尚未签收的文件或会议。虽然我们的where语句可以仅仅限制当前用户尚未签收的情况,但如果您的系统已建立了很长时间,并且数据量很大,那么,每次每个用户打开首页的时候都进行一次全表扫描,这样做意义是不大的,绝大多数的用户1个月前的文件都已经浏览过了,这样做只能徒增数据库的开销而已。事实上,我们完全可以让用户打开系统首页时,数据库仅仅查询这个用户近3个月来未阅览的文件,通过“日期”这个字段来限制表扫描,提高查询速度。如果您的办公自动化系统已经建立的2年,那么您的首页显示速度理论上将是原来速度8倍,甚至更快。
      在这里之所以提到“理论上”三字,是因为如果您的聚集索引还是盲目地建在ID这个主键上时,您的查询速度是没有这么高的,即使您在“日期”这个字段上建立的索引(非聚合索引)。下面我们就来看一下在1000万条数据量的情况下各种查询的速度表现(3个月内的数据为25万条):

    (1)仅在主键上建立聚集索引,并且不划分时间段:

    Select gid,fariqi,neibuyonghu,title from tgongwen

    用时:128470毫秒(即:128秒)

    (2)在主键上建立聚集索引,在fariq上建立非聚集索引:

    select gid,fariqi,neibuyonghu,title from Tgongwen
    where fariqi> dateadd(day,-90,getdate())

    用时:53763毫秒(54秒)

    (3)将聚合索引建立在日期列(fariqi)上:

    select gid,fariqi,neibuyonghu,title from Tgongwen
    where fariqi> dateadd(day,-90,getdate())

    用时:2423毫秒(2秒)

      虽然每条语句提取出来的都是25万条数据,各种情况的差异却是巨大的,特别是将聚集索引建立在日期列时的差异。事实上,如果您的数据库真的有1000 万容量的话,把主键建立在ID列上,就像以上的第1、2种情况,在网页上的表现就是超时,根本就无法显示。这也是我摒弃ID列作为聚集索引的一个最重要的因素。得出以上速度的方法是:在各个select语句前加:

    declare @d datetime
    set @d=getdate()

    并在select语句后加:

    select [语句执行花费时间(毫秒)]=datediff(ms,@d,getdate())

    2、只要建立索引就能显著提高查询速度
      事实上,我们可以发现上面的例子中,第2、3条语句完全相同,且建立索引的字段也相同;不同的仅是前者在fariqi字段上建立的是非聚合索引,后者在此字段上建立的是聚合索引,但查询速度却有着天壤之别。所以,并非是在任何字段上简单地建立索引就能提高查询速度。
      从建表的语句中,我们可以看到这个有着1000万数据的表中fariqi字段有5003个不同记录。在此字段上建立聚合索引是再合适不过了。在现实中,我们每天都会发几个文件,这几个文件的发文日期就相同,这完全符合建立聚集索引要求的:“既不能绝大多数都相同,又不能只有极少数相同”的规则。由此看来,我们建立“适当”的聚合索引对于我们提高查询速度是非常重要的。

    3、把所有需要提高查询速度的字段都加进聚集索引,以提高查询速度
      上面已经谈到:在进行数据查询时都离不开字段的是“日期”还有用户本身的“用户名”。既然这两个字段都是如此的重要,我们可以把他们合并起来,建立一个复合索引(compound index)。
      很多人认为只要把任何字段加进聚集索引,就能提高查询速度,也有人感到迷惑:如果把复合的聚集索引字段分开查询,那么查询速度会减慢吗?带着这个问题,我们来看一下以下的查询速度(结果集都是25万条数据):(日期列fariqi首先排在复合聚集索引的起始列,用户名neibuyonghu排在后列):

    (1)select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>''2004-5-5''

    查询速度:2513毫秒

    (2)select gid,fariqi,neibuyonghu,title from Tgongwen
                where fariqi>''2004-5-5'' and neibuyonghu=''办公室''

    查询速度:2516毫秒

    (3)select gid,fariqi,neibuyonghu,title from Tgongwen where neibuyonghu=''办公室''

    查询速度:60280毫秒

      从以上试验中,我们可以看到如果仅用聚集索引的起始列作为查询条件和同时用到复合聚集索引的全部列的查询速度是几乎一样的,甚至比用上全部的复合索引列还要略快(在查询结果集数目一样的情况下);而如果仅用复合聚集索引的非起始列作为查询条件的话,这个索引是不起任何作用的。当然,语句1、2的查询速度一样是因为查询的条目数一样,如果复合索引的所有列都用上,而且查询结果少的话,这样就会形成“索引覆盖”,因而性能可以达到最优。同时,请记住:无论您是否经常使用聚合索引的其他列,但其前导列一定要是使用最频繁的列。

    四、其他书上没有的索引使用经验总结

    1、用聚合索引比用不是聚合索引的主键速度快
      下面是实例语句:(都是提取25万条数据)

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi=''2004-9-16''

    使用时间:3326毫秒

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid<=250000

    使用时间:4470毫秒

    这里,用聚合索引比用不是聚合索引的主键速度快了近1/4。

    2、用聚合索引比用一般的主键作order by时速度快,特别是在小数据量情况下

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by fariqi

    用时:12936

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by gid

    用时:18843

      这里,用聚合索引比用一般的主键作order by时,速度快了3/10。事实上,如果数据量很小的话,用聚集索引作为排序列要比使用非聚集索引速度快得明显的多;而数据量如果很大的话,如10万以上,则二者的速度差别不明显。

    3、使用聚合索引内的时间段,搜索时间会按数据占整个数据表的百分比成比例减少,而无论聚合索引使用了多少个:

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>''2004-1-1''

    用时:6343毫秒(提取100万条)

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>''2004-6-6''

    用时:3170毫秒(提取50万条)

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi=''2004-9-16''

    用时:3326毫秒(和上句的结果一模一样。如果采集的数量一样,那么用大于号和等于号是一样的)

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen
                where fariqi>''2004-1-1'' and fariqi<''2004-6-6''

    用时:3280毫秒

    4、日期列不会因为有分秒的输入而减慢查询速度
      下面的例子中,共有100万条数据,2004年1月1日以后的数据有50万条,但只有两个不同的日期,日期精确到日;之前有数据50万条,有5000个不同的日期,日期精确到秒。

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen
              where fariqi>''2004-1-1'' order by fariqi

    用时:6390毫秒

    select gid,fariqi,neibuyonghu,reader,title from Tgongwen
                where fariqi<''2004-1-1'' order by fariqi

    用时:6453毫秒

    五、其他注意事项

      “水可载舟,亦可覆舟”,索引也一样。索引有助于提高检索性能,但过多或不当的索引也会导致系统低效。因为用户在表中每加进一个索引,数据库就要做更多的工作。过多的索引甚至会导致索引碎片。
      所以说,我们要建立一个“适当”的索引体系,特别是对聚合索引的创建,更应精益求精,以使您的数据库能得到高性能的发挥。
      当然,在实践中,作为一个尽职的数据库管理员,您还要多测试一些方案,找出哪种方案效率最高、最为有效。



原文
地址:https://yq.aliyun.com/articles/64352
月初在云栖社区上发起了一个 MongoDB 使用场景及运维管理问题交流探讨 的技术话题,有近5000人关注了该话题讨论,这里就 MongoDB 的使用场景做个简单的总结,谈谈什么场景该用 MongoDB?
很多人比较关心 MongoDB 的适用场景,也有用户在话题里分享了自己的业务场景,比如
案例1
用在应用服务器的日志记录,查找起来比文本灵活,导出也很方便。也是给应用练手,从外围系统开始使用MongoDB。
  1. 用在一些第三方信息的获取或者抓取,因为MongoDB的schema-less,所有格式灵活,不用为了各种格式不一样的信息专门设计统一的格式,极大的减少开发的工作。
案例2
mongodb之前有用过,主要用来存储一些监控数据,No schema 对开发人员来说,真的很方便,增加字段不用改表结构,而且学习成本极低。
案例3
使用MongoDB做了O2O快递应用,·将送快递骑手、快递商家的信息(包含位置信息)存储在 MongoDB,然后通过 MongoDB 的地理位置查询,这样很方便的实现了查找附近的商家、骑手等功能,使得快递骑手能就近接单,目前在使用MongoDB 上没遇到啥大的问题,官网的文档比较详细,很给力。
经常跟一些同学讨论 MongoDB 业务场景时,会听到类似『你这个场景 mysql 也能解决,没必要一定用 MongoDB』的声音,的确,并没有某个业务场景必须要使用 MongoDB才能解决,但使用 MongoDB 通常能让你以更低的成本解决问题(包括学习、开发、运维等成本),下面是 MongoDB 的主要特性,大家可以对照自己的业务需求看看,匹配的越多,用 MongoDB 就越合适。
MongoDB 特性
优势
事务支持
MongoDB 目前只支持单文档事务,需要复杂事务支持的场景暂时不适合
灵活的文档模型
JSON 格式存储最接近真实对象模型,对开发者友好,方便快速开发迭代
高可用复制集
满足数据高可靠、服务高可用的需求,运维简单,故障自动切换
可扩展分片集群
海量数据存储,服务能力水平扩展
高性能
mmapv1、wiredtiger、mongorocks(rocksdb)、in-memory 等多引擎支持满足各种场景需求
强大的索引支持
地理位置索引可用于构建 各种 O2O 应用、文本索引解决搜索的需求、TTL索引解决历史数据自动过期的需求
Gridfs
解决文件存储的需求
aggregation & mapreduce
解决数据分析场景需求,用户可以自己写查询语句或脚本,将请求都分发到 MongoDB 上完成
从目前阿里云 MongoDB 云数据库上的用户看,MongoDB 的应用已经渗透到各个领域,比如游戏、物流、电商、内容管理、社交、物联网、视频直播等,以下是几个实际的应用案例。
如果你还在为是否应该使用 MongoDB,不如来做几个选择题来辅助决策(注:以下内容改编自 MongoDB 公司 TJ 同学的某次公开技术分享)。
应用特征
Yes / No
应用不需要事务及复杂 join 支持
必须 Yes
新应用,需求会变,数据模型无法确定,想快速迭代开发
应用需要2000-3000以上的读写QPS(更高也可以)
应用需要TB甚至 PB 级别数据存储
?
应用发展迅速,需要能快速水平扩展
?
应用要求存储的数据不丢失
?
应用需要99.999%高可用
?
应用需要大量的地理位置查询、文本查询
如果上述有1个 Yes,可以考虑 MongoDB,2个及以上的 Yes,选择MongoDB绝不会后悔。

了解了ES的使用场景,ES的研究、使用、推广才更有价值和意义。

1、场景—:使用Elasticsearch作为主要的后端

传统项目中,搜索引擎是部署在成熟的数据存储的顶部,以提供快速且相关的搜索能力。这是因为早期的搜索引擎不能提供耐用的存储或其他经常需要的功能,如统计。 
 
Elasticsearch是提供持久存储、统计等多项功能的现代搜索引擎。 
如果你开始一个新项目,我们建议您考虑使用Elasticsearch作为唯一的数据存储,以帮助保持你的设计尽可能简单。 
此种场景不支持包含频繁更新、事务(transaction)的操作。
举例如下:新建一个博客系统使用es作为存储。 
1)我们可以向ES提交新的博文; 
2)使用ES检索、搜索、统计数据。
ES作为存储的优势: 
如果一台服务器出现故障时会发生什么?你可以通过复制 数据到不同的服务器以达到容错的目的。 
注意: 
整体架构设计时,需要我们权衡是否有必要增加额外的存储。

2、场景二:在现有系统中增加elasticsearch

由于ES不能提供存储的所有功能,一些场景下需要在现有系统数据存储的基础上新增ES支持。 
 
举例1:ES不支持事务、复杂的关系(至少1.X版本不支持,2.X有改善,但支持的仍然不好),如果你的系统中需要上述特征的支持,需要考虑在原有架构、原有存储的基础上的新增ES的支持。
举例2:如果你已经有一个在运行的复杂的系统,你的需求之一是在现有系统中添加检索服务。一种非常冒险的方式是重构系统以支持ES。而相对安全的方式是:将ES作为新的组件添加到现有系统中。 
如果你使用了如下图所示的SQL数据库和ES存储,你需要找到一种方式使得两存储之间实时同步。需要根据数据的组成、数据库选择对应的同步插件。可供选择的插件包括: 
1)mysql、oracle选择 logstash-input-jdbc 插件。 
2)mongo选择 mongo-connector工具。
假设你的在线零售商店的产品信息存储在SQL数据库中。 为了快速且相关的搜索,你安装Elasticsearch。 
为了索引数据,您需要部署一个同步机制,该同步机制可以是Elasticsearch插件或你建立一个自定义的服务。此同步机制可以将对应于每个产品的所有数据和索引都存储在Elasticsearch,每个产品作为一个document存储(这里的document相当于关系型数据库中的一行/row数据)。
当在该网页上的搜索条件中输入“用户的类型”,店面网络应用程序通过Elasticsearch查询该信息。 Elasticsearch返回符合标准的产品documents,并根据你喜欢的方式来分类文档。 排序可以根据每个产品的被搜索次数所得到的相关分数,或任何存储在产品document信息,例如:最新最近加入的产品、平均得分,或者是那些插入或更新信息。 所以你可以只使用Elasticsearch处理搜索。这取决于同步机制来保持Elasticsearch获取最新变化。

3、场景三:使用elasticsearch和现有的工具

在一些使用情况下,您不必写一行代码就能通过elasticssearch完成一项工作。很多工具都可以与Elasticsearch一起工作,所以你不必到你从头开始编写。 
例如,假设要部署一个大规模的日志框架存储,搜索,并分析了大量的事件。 
如图下图,处理日志和输出到Elasticsearch,您可以使用日志记录工具,如rsyslog(www.rsyslog.com),Logstash(www.elastic.co/products/logstash),或Apache Flume(http://flume.apache.org)。 
搜索和可视化界面分析这些日志,你可以使用Kibana(www.elastic.co/产品/ kibana)。 

为什么那么多工具适配Elasticsearch?主要原因如下:

1)Elasticsearch是开源的。 
2)Elasticsearch提供了JAVA API接口。 
3)Elasticsearch提供了RESTful API接口(不管程序用什么语言开发,任何程序都可以访问) 
4)更重要的是,REST请求和应答是典型的JSON(JavaScript对象 符号)格式。通常情况下,一个REST请求包含一个JSON文件,其回复都 也是一个JSON文件。
原文地址:http://blog.csdn.net/laoyang360/article/details/52227541

基本概念
(1)数据库事务是具有ACID特性的一种数据库操作逻辑,解决不同事务的并发问题的时候需要根据不同并发问题设置不同的隔离级别,而不同的隔离级别的实现就是用的不同的数据库锁策略,包括行锁,表锁,页锁等
(2)数据库锁一般不需要我们显式调用,数据库事务及其隔离级别在底层已经通过锁给我们做好了解决各种并发问题的措施,几种隔离级别分别可以解决脏读、不可重复读、幻读和丢失更新的并发问题,所以一般我们不需要去使用锁(悲观锁和乐观锁相关策略除外),只需要使用好事务及其隔离级别即可
(3)丢失更新虽然可以使用串行化隔离级别的事务来解决,不过因为其效率太低,所以一般是使用乐观锁策略或者悲观锁策略来解决该问题
注意:不可重复读的重点在于update和delete,而幻读的重点在于insert,innodb引擎的可重复读同时解决脏读、不可重复读和幻读问题。

事务的四大特性
(1)原子性(Atomicity)
   原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
(2)一致性(Consistency)
   一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是一致性。
(3)隔离性(Isolation)
   隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行,数据库提供了多种隔离级别来应对各种并发需求。
(4)持久性(Durability)
   持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

数据库并发的问题
(1)脏读
   脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
   当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。
(2)不可重复读
 不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
 不可重复读和脏读的区别是:脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
   在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。
(3)虚读(幻读)
  幻读是事务非独立执行时发生的一种现象。事务A读的时候读出了15条记录,事务B在事务A执行的过程中增加了1条,事务A再读的时候就变成了 16 条,这种情况就叫做幻影读。
  幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
(4)丢失更新
   两个不同事务同时获得相同数据,然后在各自事务中同时修改了该数据(该修改对旧数据有依赖性),那么先提交的事务更新会被后提交事务的更新给覆盖掉,这种情况下事务A的更新就被覆盖掉了、丢失了。比如取值-修改-写完,带判断的逻辑的修改等等。

数据库事务隔离级别解决的问题

隔离级别
脏读
不可重复读
幻读 
丢失更新
未提交读
可能
可能
可能
可能
已提交读
不可能
可能
可能
可能
可重复读
不可能
不可能
可能
可能
串行化
不可能
不可能
不可能
不可能

    注意:
    从上到下越来越安全,但是并发性和效率越来越低,MySQL缺省隔离级别是是"可重复读"(REPEATABLE READ)
    串行化一般是尽量避免使用,所以解决丢失更新问题尽量是使用悲观锁或者乐观锁。

事务隔离级别通过锁的实现机制
    两个锁:排他锁(与任何锁互斥)、共享锁(与共享锁不互斥,与排他锁互斥)
    在运用排他锁和共享锁对数据对象加锁时,还需要约定一些规则,例如何时申请排他锁或共享锁、持锁时间、何时释放等。这些规被称为封锁协议(LockingProtocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。

参考文章
(1)数据库事务隔离级别和锁实现机制:http://blog.csdn.net/flyingfalcon/article/details/53045672
(2)数据库隔离级别及其实现原理:http://www.cnblogs.com/wajika/p/6680200.html
(3)数据库事务与锁的关系:http://m.blog.csdn.net/dreamwbt/article/details/53371687

个人理解
(1)MySQL数据库服务器包含多种存储引擎:MyISAM,BDB,InnoDB等,每种存储引擎有自己的锁策略。
   MyISAM:支持表锁,不支持事务,不适合于有大量查询&修改并存(或者说不适合修改)的情况,特别适合只读数据库
   InnoDB:支持表锁和行锁,支持事务
   BDB:支持表锁和页锁,支持事务
(2)共享锁在一般情况下作为读锁,排它锁在一般情况下作为写锁。但也有例外,比如读未提交隔离级别的写锁是共享锁!
(3)数据库的很多锁都有独占和共享两个版本,行锁、页锁等分类方式只是针对范围进行分类,共享和互斥是针对同步策略分类
(4)平时我们不直接使用事务,只是单独的sql语句时,数据库引擎都会保证sql语句的执行语句的安全性,都会加锁,比如对于MyISAM,每个读操作都会加表读锁,每个写操作都会加表写锁,InnoDB会使用隐式事务,不过此种事务粒度是单个sql,因此在上层逻辑中连续执行,并发会出现错误。
(5)锁我们一般不需要直接调用,使用事务可以为我们屏蔽数据库内部锁的实现细节,所以我们编程实现时好好使用事务和事务隔离级别即可,不过对于并发更新问题,一般不会使用串行化事务去解决,一般使用悲观锁和乐观锁策略,在使用悲观锁策略时,就需要程序员手动调用锁
(6)Mysql的行锁基于索引,无法使用索引时会直接使用表锁!
(7)数据库服务器比我们想象的复杂得多且相当成熟,解决数据库的并发问题一般不需要花多心思去解决

原文
地址:http://blog.csdn.net/mysteryhaohao/article/details/51669741
锁,在现实生活中是为我们想要隐藏于外界所使用的一种工具。在计算机中,是协调多个进程或县城并发访问某一资源的一种机制。在数据库当中,除了传统的计算资源(CPU、RAM、I/O等等)的争用之外,数据也是一种供许多用户共享访问的资源。如何保证数据并发访问的一致性、有效性,是所有数据库必须解决的一个问题,锁的冲突也是影响数据库并发访问性能的一个重要因素。从这一角度来说,锁对于数据库而言就显得尤为重要。
MySQL锁
相对于其他的数据库而言,MySQL的锁机制比较简单,最显著的特点就是不同的存储引擎支持不同的锁机制。根据不同的存储引擎,MySQL中锁的特性可以大致归纳如下:

行锁
表锁
页锁
MyISAM


BDB

InnoDB


开销、加锁速度、死锁、粒度、并发性能
表锁: 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
行锁: 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
页锁: 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般
从上述的特点课件,很难笼统的说哪种锁最好,只能根据具体应用的特点来说哪种锁更加合适。仅仅从锁的角度来说的话:
表锁更适用于以查询为主,只有少量按索引条件更新数据的应用;行锁更适用于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用。(PS:由于BDB已经被InnoDB所取代,我们只讨论MyISAM表锁和InnoDB行锁的问题)
MyISAM表锁
MyISAM存储引擎只支持表锁,这也是MySQL开始几个版本中唯一支持的锁类型。随着应用对事务完整性和并发性要求的不断提高,MySQL才开始开发基于事务的存储引擎,后来慢慢出现了支持页锁的BDB存储引擎和支持行锁的InnoDB存储引擎(实际 InnoDB是单独的一个公司,现在已经被Oracle公司收购)。但是MyISAM的表锁依然是使用最为广泛的锁类型。本节将详细介绍MyISAM表锁的使用。
查询表级锁争用情况
可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定争夺:
mysql> show status like 'table%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Table_locks_immediate | 2979  |
| Table_locks_waited    | 0     |
+-----------------------+-------+
2 rows in set (0.00 sec))
如果Table_locks_waited的值比较高,则说明存在着较严重的表级锁争用情况。

MySQL表级锁的锁模式

MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。锁模式的兼容性如下表所示。
                                          MySQL中的表锁兼容性                
请求锁模式
         是否兼容
当前锁模式
None
读锁
写锁
读锁
写锁
可见,对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的!根据如下表所示的例子可以知道,当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。
                        MyISAM存储引擎的写阻塞读例子
session_1
session_2
获得表film_text的WRITE锁定
mysql> lock table film_text write;
Query OK, 0 rows affected (0.00 sec)

当前session对锁定表的查询、更新、插入操作都可以执行:
mysql> select film_id,title from film_text where film_id = 1001;
+---------+-------------+
| film_id | title       |
+---------+-------------+
| 1001    | Update Test |
+---------+-------------+
1 row in set (0.00 sec)
mysql> insert into film_text (film_id,title) values(1003,'Test');
Query OK, 1 row affected (0.00 sec)
mysql> update film_text set title = 'Test' where film_id = 1001;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
其他session对锁定表的查询被阻塞,需要等待锁被释放:
mysql> select film_id,title from film_text where film_id = 1001;
等待
释放锁:
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
等待

Session2获得锁,查询返回:
mysql> select film_id,title from film_text where film_id = 1001;
+---------+-------+
| film_id | title |
+---------+-------+
| 1001    | Test  |
+---------+-------+
1 row in set (57.59 sec)


如何加表锁

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。在本书的示例中,显式加锁基本上都是为了方便而已,并非必须如此。
给MyISAM表显示加锁,一般是为了在一定程度模拟事务操作,实现对某一时间点多个表的一致性读取。例如,有一个订单表orders,其中记录有各订单的总金额total,同时还有一个订单明细表order_detail,其中记录有各订单每一产品的金额小计 subtotal,假设我们需要检查这两个表的金额合计是否相符,可能就需要执行如下两条SQL:
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
这时,如果不先给两个表加锁,就可能产生错误的结果,因为第一条语句执行过程中,order_detail表可能已经发生了改变。因此,正确的方法应该是:
Lock tables orders read local, order_detail read local;
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
Unlock tables;
要特别说明以下两点内容。
上面的例子在LOCK TABLES时加了“local”选项,其作用就是在满足MyISAM表并发插入条件的情况下,允许其他用户在表尾并发插入记录,有关MyISAM表的并发插入问题,在后面的章节中还会进一步介绍。
在用LOCK TABLES给表显式加表锁时,必须同时取得所有涉及到表的锁,并且MySQL不支持锁升级。也就是说,在执行LOCK TABLES后,只能访问显式加锁的这些表,不能访问未加锁的表;同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。其实,在自动加锁的情况下也基本如此,MyISAM总是一次获得SQL语句所需要的全部锁。这也正是MyISAM表不会出现死锁(Deadlock Free)的原因。
在如下表所示的例子中,一个session使用LOCK TABLE命令给表film_text加了读锁,这个session可以查询锁定表中的记录,但更新或访问其他表都会提示错误;同时,另外一个session可以查询表中的记录,但更新就会出现锁等待。
MyISAM存储引擎的读阻塞写例子
session_1
session_2
获得表film_text的READ锁定
mysql> lock table film_text read;
Query OK, 0 rows affected (0.00 sec)

当前session可以查询该表记录
mysql> select film_id,title from film_text where film_id = 1001;
+---------+------------------+
| film_id | title            |
+---------+------------------+
| 1001    | ACADEMY DINOSAUR |
+---------+------------------+
1 row in set (0.00 sec)
其他session也可以查询该表的记录
mysql> select film_id,title from film_text where film_id = 1001;
+---------+------------------+
| film_id | title            |
+---------+------------------+
| 1001    | ACADEMY DINOSAUR |
+---------+------------------+
1 row in set (0.00 sec)
当前session不能查询没有锁定的表
mysql> select film_id,title from film where film_id = 1001;
ERROR 1100 (HY000): Table 'film' was not locked with LOCK TABLES
其他session可以查询或者更新未锁定的表
mysql> select film_id,title from film where film_id = 1001;
+---------+---------------+
| film_id | title         |
+---------+---------------+
| 1001    | update record |
+---------+---------------+
1 row in set (0.00 sec)
mysql> update film set title = 'Test' where film_id = 1001;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0
当前session中插入或者更新锁定的表都会提示错误:
mysql> insert into film_text (film_id,title) values(1002,'Test');
ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated
mysql> update film_text set title = 'Test' where film_id = 1001;
ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated
其他session更新锁定表会等待获得锁:
mysql> update film_text set title = 'Test' where film_id = 1001;
等待
释放锁
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
等待

Session获得锁,更新操作完成:
mysql> update film_text set title = 'Test' where film_id = 1001;
Query OK, 1 row affected (1 min 0.71 sec)
Rows matched: 1  Changed: 1  Warnings: 0

注意,当使用LOCK TABLES时,不仅需要一次锁定用到的所有表,而且,同一个表在SQL语句中出现多少次,就要通过与SQL语句中相同的别名锁定多少次,否则也会出错!举例说明如下。
(1)对actor表获得读锁:
mysql> lock table actor read;
Query OK, 0 rows affected (0.00 sec)
(2)但是通过别名访问会提示错误:
mysql> select a.first_name,a.last_name,b.first_name,b.last_name from actor a,actor b where a.first_name = b.first_name and a.first_name = 'Lisa' and a.last_name = 'Tom' and a.last_name <> b.last_name;
ERROR 1100 (HY000): Table 'a' was not locked with LOCK TABLES
(3)需要对别名分别锁定:
mysql> lock table actor as a read,actor as b read;
Query OK, 0 rows affected (0.00 sec)
(4)按照别名的查询可以正确执行:
mysql> select a.first_name,a.last_name,b.first_name,b.last_name from actor a,actor b where a.first_name = b.first_name and a.first_name = 'Lisa' and a.last_name = 'Tom' and a.last_name <> b.last_name;
+------------+-----------+------------+-----------+
| first_name | last_name | first_name | last_name |
+------------+-----------+------------+-----------+
| Lisa       | Tom       | LISA       | MONROE    |
+------------+-----------+------------+-----------+
1 row in set (0.00 sec)

并发插入(Concurrent Inserts)

上文提到过MyISAM表的读和写是串行的,但这是就总体而言的。在一定条件下,MyISAM表也支持查询和插入操作的并发进行。
MyISAM存储引擎有一个系统变量concurrent_insert,专门用以控制其并发插入的行为,其值分别可以为0、1或2。
当concurrent_insert设置为0时,不允许并发插入。
当concurrent_insert设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL的默认设置。
当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。
在如下表所示的例子中,session_1获得了一个表的READ LOCAL锁,该线程可以对表进行查询操作,但不能对表进行更新操作;其他的线程(session_2),虽然不能对表进行删除和更新操作,但却可以对该表进行并发插入操作,这里假设该表中间不存在空洞。
              MyISAM存储引擎的读写(INSERT)并发例子
session_1
session_2
获得表film_text的READ LOCAL锁定
mysql> lock table film_text read local;
Query OK, 0 rows affected (0.00 sec)

当前session不能对锁定表进行更新或者插入操作:
mysql> insert into film_text (film_id,title) values(1002,'Test');
ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated
mysql> update film_text set title = 'Test' where film_id = 1001;
ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated
其他session可以进行插入操作,但是更新会等待:
mysql> insert into film_text (film_id,title) values(1002,'Test');
Query OK, 1 row affected (0.00 sec)
mysql> update film_text set title = 'Update Test' where film_id = 1001;
等待
当前session不能访问其他session插入的记录:
mysql> select film_id,title from film_text where film_id = 1002;
Empty set (0.00 sec)

释放锁:
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
等待
当前session解锁后可以获得其他session插入的记录:
mysql> select film_id,title from film_text where film_id = 1002;
+---------+-------+
| film_id | title |
+---------+-------+
| 1002    | Test  |
+---------+-------+
1 row in set (0.00 sec)
Session2获得锁,更新操作完成:
mysql> update film_text set title = 'Update Test' where film_id = 1001;
Query OK, 1 row affected (1 min 17.75 sec)
Rows matched: 1  Changed: 1  Warnings: 0
可以利用MyISAM存储引擎的并发插入特性,来解决应用中对同一表查询和插入的锁争用。例如,将concurrent_insert系统变量设为2,总是允许并发插入;同时,通过定期在系统空闲时段执行 OPTIMIZE TABLE语句来整理空间碎片,收回因删除记录而产生的中间空洞。有关OPTIMIZE TABLE语句的详细介绍,可以参见第18章中“两个简单实用的优化方法”一节的内容。

MyISAM的锁调度
前面讲过,MyISAM存储引擎的读锁和写锁是互斥的,读写操作是串行的。那么,一个进程请求某个 MyISAM表的读锁,同时另一个进程也请求同一表的写锁,MySQL如何处理呢?答案是写进程先获得锁。不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!这是因为MySQL认为写请求一般比读请求要重要。这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。这种情况有时可能会变得非常糟糕!幸好我们可以通过一些设置来调节MyISAM 的调度行为。
通过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。
通过执行命令SET LOW_PRIORITY_UPDATES=1,使该连接发出的更新请求优先级降低。
通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。
虽然上面3种方法都是要么更新优先,要么查询优先的方法,但还是可以用其来解决查询相对重要的应用(如用户登录系统)中,读锁等待严重的问题。
另外,MySQL也提供了一种折中的办法来调节读写冲突,即给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,给读进程一定获得锁的机会。
上面已经讨论了写优先调度机制带来的问题和解决办法。这里还要强调一点:一些需要长时间运行的查询操作,也会使写进程“饿死”!因此,应用中应尽量避免出现长时间运行的查询操作,不要总想用一条SELECT语句来解决问题,因为这种看似巧妙的SQL语句,往往比较复杂,执行时间较长,在可能的情况下可以通过使用中间表等措施对SQL语句做一定的“分解”,使每一步查询都能在较短时间完成,从而减少锁冲突。如果复杂查询不可避免,应尽量安排在数据库空闲时段执行,比如一些定期统计可以安排在夜间执行。

InnoDB锁问题
InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。行级锁与表级锁本来就有许多不同之处,另外,事务的引入也带来了一些新问题。下面我们先介绍一点背景知识,然后详细讨论InnoDB的锁问题。
背景知识
1.事务(Transaction)及其ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
银行转帐就是事务的一个典型例子。
2.并发事务处理带来的问题
相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况。
更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题。
脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。
不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
3.事务隔离级别
在上面讲到的并发事务处理带来的问题中,“更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。数据库实现事务隔离的方式,基本上可分为以下两种。
一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。
另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上 “串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。
为了解决“隔离”与“并发”的矛盾,ISO/ANSI SQL92定义了4个事务隔离级别,每个级别的隔离程度不同,允许出现的副作用也不同,应用可以根据自己的业务逻辑要求,通过选择不同的隔离级别来平衡 “隔离”与“并发”的矛盾。下表很好地概括了这4个隔离级别的特性。
                                            4种隔离级别比较
读数据一致性及允许的并发副作用
隔离级别
读数据一致性
脏读
不可重复读
幻读
未提交读(Read uncommitted)
最低级别,只能保证不读取物理上损坏的数据
已提交度(Read committed)
语句级
可重复读(Repeatable read)
事务级
可序列化(Serializable)
最高级别,事务级
最后要说明的是:各具体数据库并不一定完全实现了上述4个隔离级别,例如,Oracle只提供Read committed和Serializable两个标准隔离级别,另外还提供自己定义的Read only隔离级别;SQL Server除支持上述ISO/ANSI SQL92定义的4个隔离级别外,还支持一个叫做“快照”的隔离级别,但严格来说它是一个用MVCC实现的Serializable隔离级别。MySQL 支持全部4个隔离级别,但在具体实现时,有一些特点,比如在一些隔离级别下是采用MVCC一致性读,但某些情况下又不是,这些内容在后面的章节中将会做进一步介绍。

获取InnoDB行锁争用情况    
可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| InnoDB_row_lock_current_waits | 0     |
| InnoDB_row_lock_time          | 0     |
| InnoDB_row_lock_time_avg      | 0     |
| InnoDB_row_lock_time_max      | 0     |
| InnoDB_row_lock_waits         | 0     |
+-------------------------------+-------+
5 rows in set (0.01 sec)
如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。
具体方法如下:
mysql> CREATE TABLE innodb_monitor(a INT) ENGINE=INNODB;
Query OK, 0 rows affected (0.14 sec)
然后就可以用下面的语句来进行查看:
mysql> Show innodb status\G;
*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
------------
TRANSACTIONS
------------
Trx id counter 0 117472192
Purge done for trx's n:o < 0 117472190 undo n:o < 0 0
History list length 17
Total number of lock structs in row lock hash table 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 0 117472185, not started, process no 11052, OS thread id 1158191456
MySQL thread id 200610, query id 291197 localhost root
---TRANSACTION 0 117472183, not started, process no 11052, OS thread id 1158723936
MySQL thread id 199285, query id 291199 localhost root
Show innodb status
监视器可以通过发出下列语句来停止查看:
mysql> DROP TABLE innodb_monitor;
Query OK, 0 rows affected (0.05 sec)
设置监视器后,在SHOW INNODB STATUS的显示内容中,会有详细的当前锁等待的信息,包括表名、锁类型、锁定记录的情况等,便于进行进一步的分析和问题的确定。打开监视器以后,默认情况下每15秒会向日志中记录监控的内容,如果长时间打开会导致.err文件变得非常的巨大,所以用户在确认问题原因之后,要记得删除监控表以关闭监视器,或者通过使用“--console”选项来启动服务器以关闭写日志文件。

InnoDB的行锁模式及加锁方法
InnoDB实现了以下两种类型的行锁。
共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
上述锁模式的兼容情况具体如下表所示。
                                         InnoDB行锁模式兼容性列表
请求锁模式
   是否兼容
当前锁模式
X
IX
S
IS
X
冲突
冲突
冲突
冲突
IX
冲突
兼容
冲突
兼容
S
冲突
冲突
兼容
兼容
IS
冲突
兼容
兼容
兼容
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。
意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。
共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
用SELECT ... IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT... FOR UPDATE方式获得排他锁。
在如下表所示的例子中,使用了SELECT ... IN SHARE MODE加锁后再更新记录,看看会出现什么情况,其中actor表的actor_id字段为主键。
  InnoDB存储引擎的共享锁例子
session_1
session_2
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where actor_id = 178;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
| 178      | LISA       | MONROE    |
+----------+------------+-----------+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where actor_id = 178;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
| 178      | LISA       | MONROE    |
+----------+------------+-----------+
1 row in set (0.00 sec)
当前session对actor_id=178的记录加share mode 的共享锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 178lock in share mode;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
| 178      | LISA       | MONROE    |
+----------+------------+-----------+
1 row in set (0.01 sec)


其他session仍然可以查询记录,并也可以对该记录加share mode的共享锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 178lock in share mode;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
| 178      | LISA       | MONROE    |
+----------+------------+-----------+
1 row in set (0.01 sec)
当前session对锁定的记录进行更新操作,等待锁:
mysql> update actor set last_name = 'MONROE T' where actor_id = 178;
等待


其他session也对该记录进行更新操作,则会导致死锁退出:
mysql> update actor set last_name = 'MONROE T' where actor_id = 178;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
获得锁后,可以成功更新:
mysql> update actor set last_name = 'MONROE T' where actor_id = 178;
Query OK, 1 row affected (17.67 sec)
Rows matched: 1  Changed: 1  Warnings: 0

    当使用SELECT...FOR UPDATE加锁后再更新记录,出现如下表所示的情况。
 InnoDB存储引擎的排他锁例子
session_1
session_2
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where actor_id = 178;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
| 178      | LISA       | MONROE    |
+----------+------------+-----------+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where actor_id = 178;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
| 178      | LISA       | MONROE    |
+----------+------------+-----------+
1 row in set (0.00 sec)
当前session对actor_id=178的记录加for update的排它锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
| 178      | LISA       | MONROE    |
+----------+------------+-----------+
1 row in set (0.00 sec)


其他session可以查询该记录,但是不能对该记录加共享锁,会等待获得锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 178;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
| 178      | LISA       | MONROE    |
+----------+------------+-----------+
1 row in set (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update;
等待
当前session可以对锁定的记录进行更新操作,更新后释放锁:
mysql> update actor set last_name = 'MONROE T' where actor_id = 178;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.01 sec)


其他session获得锁,得到其他session提交的记录:
mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update;
+----------+------------+-----------+
| actor_id | first_name | last_name |
+----------+------------+-----------+
| 178      | LISA       | MONROE T  |
+----------+------------+-----------+
1 row in set (9.59 sec)

InnoDB行锁实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。
(1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
在如下所示的例子中,开始tab_no_index表没有索引:
mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb;
Query OK, 0 rows affected (0.15 sec)
mysql> insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4');
Query OK, 4 rows affected (0.00 sec)
Records: 4  Duplicates: 0  Warnings: 0
   InnoDB存储引擎的表在不使用索引时使用表锁例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_no_index where id = 1 ;
+------+------+
| id   | name |
+------+------+
| 1    | 1    |
+------+------+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_no_index where id = 2 ;
+------+------+
| id   | name |
+------+------+
| 2    | 2    |
+------+------+
1 row in set (0.00 sec)
mysql> select * from tab_no_index where id = 1 for update;
+------+------+
| id   | name |
+------+------+
| 1    | 1    |
+------+------+
1 row in set (0.00 sec)


mysql> select * from tab_no_index where id = 2 for update;
等待
在如上表所示的例子中,看起来session_1只给一行加了排他锁,但session_2在请求其他行的排他锁时,却出现了锁等待!原因就是在没有索引的情况下,InnoDB只能使用表锁。当我们给其增加一个索引后,InnoDB就只锁定了符合条件的行,如下表所示。
创建tab_with_index表,id字段有普通索引:
mysql> create table tab_with_index(id int,name varchar(10)) engine=innodb;
Query OK, 0 rows affected (0.15 sec)
mysql> alter table tab_with_index add index id(id);
Query OK, 4 rows affected (0.24 sec)
Records: 4  Duplicates: 0  Warnings: 0
   InnoDB存储引擎的表在使用索引时使用行锁例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_with_index where id = 1 ;
+------+------+
| id   | name |
+------+------+
| 1    | 1    |
+------+------+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_with_index where id = 2 ;
+------+------+
| id   | name |
+------+------+
| 2    | 2    |
+------+------+
1 row in set (0.00 sec)
mysql> select * from tab_with_index where id = 1 for update;
+------+------+
| id   | name |
+------+------+
| 1    | 1    |
+------+------+
1 row in set (0.00 sec)


mysql> select * from tab_with_index where id = 2 for update;
+------+------+
| id   | name |
+------+------+
| 2    | 2    |
+------+------+
1 row in set (0.00 sec)
(2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。
在如下表所示的例子中,表tab_with_index的id字段有索引,name字段没有索引:
mysql> alter table tab_with_index drop index name;
Query OK, 4 rows affected (0.22 sec)
Records: 4  Duplicates: 0  Warnings: 0
mysql> insert into tab_with_index  values(1,'4');
Query OK, 1 row affected (0.00 sec)
mysql> select * from tab_with_index where id = 1;
+------+------+
| id   | name |
+------+------+
| 1    | 1    |
| 1    | 4    |
+------+------+
2 rows in set (0.00 sec)
   InnoDB存储引擎使用相同索引键的阻塞例子       
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_with_index where id = 1 and name = '1' for update;
+------+------+
| id   | name |
+------+------+
| 1    | 1    |
+------+------+
1 row in set (0.00 sec)


虽然session_2访问的是和session_1不同的记录,但是因为使用了相同的索引,所以需要等待锁:
mysql> select * from tab_with_index where id = 1 and name = '4' for update;
等待
(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
在如下表所示的例子中,表tab_with_index的id字段有主键索引,name字段有普通索引:
mysql> alter table tab_with_index add index name(name);
Query OK, 5 rows affected (0.23 sec)
Records: 5  Duplicates: 0  Warnings: 0
  InnoDB存储引擎的表使用不同索引的阻塞例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_with_index where id = 1 for update;
+------+------+
| id   | name |
+------+------+
| 1    | 1    |
| 1    | 4    |
+------+------+
2 rows in set (0.00 sec)


Session_2使用name的索引访问记录,因为记录没有被索引,所以可以获得锁:
mysql> select * from tab_with_index where name = '2' for update;
+------+------+
| id   | name |
+------+------+
| 2    | 2    |
+------+------+
1 row in set (0.00 sec)

由于访问的记录已经被session_1锁定,所以等待获得锁。:
mysql> select * from tab_with_index where name = '4' for update;
(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。

在下面的例子中,检索值的数据类型与索引字段不同,虽然MySQL能够进行数据类型转换,但却不会使用索引,从而导致InnoDB使用表锁。通过用explain检查两条SQL的执行计划,我们可以清楚地看到了这一点。
例子中tab_with_index表的name字段有索引,但是name字段是varchar类型的,如果where条件中不是和varchar类型进行比较,则会对name进行类型转换,而执行的全表扫描。
mysql> alter table tab_no_index add index name(name);
Query OK, 4 rows affected (8.06 sec)
Records: 4  Duplicates: 0  Warnings: 0
mysql> explain select * from tab_with_index where name = 1 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tab_with_index
         type: ALL
possible_keys: name
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
        Extra: Using where
1 row in set (0.00 sec)
mysql> explain select * from tab_with_index where name = '1' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tab_with_index
         type: ref
possible_keys: name
          key: name
      key_len: 23
          ref: const
         rows: 1
        Extra: Using where
1 row in set (0.00 sec)

间隙锁(Next-Key锁)
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,...,100,101,下面的SQL:
Select * from  emp where empid > 100 for update;
是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,要是不使用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另外一方面,是为了满足其恢复和复制的需要。有关其恢复和复制对锁机制的影响,以及不同隔离级别下InnoDB使用间隙锁的情况,在后续的章节中会做进一步介绍。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
还要特别说明的是,InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!
在如下表所示的例子中,假如emp表中只有101条记录,其empid的值分别是1,2,......,100,101。
               InnoDB存储引擎的间隙锁阻塞例子
session_1
session_2
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
当前session对不存在的记录加for update的锁:
mysql> select * from emp where empid = 102 for update;
Empty set (0.00 sec)


这时,如果其他session插入empid为102的记录(注意:这条记录并不存在),也会出现锁等待:
mysql>insert into emp(empid,...) values(102,...);
阻塞等待
Session_1 执行rollback:
mysql> rollback;
Query OK, 0 rows affected (13.04 sec)


由于其他session_1回退后释放了Next-Key锁,当前session可以获得锁并成功插入记录:
mysql>insert into emp(empid,...) values(102,...);
Query OK, 1 row affected (13.35 sec)

恢复和复制的需要,对InnoDB锁机制的影响
MySQL通过BINLOG录执行成功的INSERT、UPDATE、DELETE等更新数据的SQL语句,并由此实现MySQL数据库的恢复和主从复制(可以参见本书“管理篇”的介绍)。MySQL的恢复机制(复制其实就是在Slave Mysql不断做基于BINLOG的恢复)有以下特点。
l  一是MySQL的恢复是SQL语句级的,也就是重新执行BINLOG中的SQL语句。这与Oracle数据库不同,Oracle是基于数据库文件块的。
l  二是MySQL的Binlog是按照事务提交的先后顺序记录的,恢复也是按这个顺序进行的。这点也与Oralce不同,Oracle是按照系统更新号(System Change Number,SCN)来恢复数据的,每个事务开始时,Oracle都会分配一个全局唯一的SCN,SCN的顺序与事务开始的时间顺序是一致的。
从上面两点可知,MySQL的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读,这已经超过了ISO/ANSI SQL92“可重复读”隔离级别的要求,实际上是要求事务要串行化。这也是许多情况下,InnoDB要用到间隙锁的原因,比如在用范围条件更新记录时,无论在Read Commited或是Repeatable Read隔离级别下,InnoDB都要使用间隙锁,但这并不是隔离级别要求的,有关InnoDB在不同隔离级别下加锁的差异在下一小节还会介绍。
另外,对于“insert  into target_tab select * from source_tab where ...”和“create  table new_tab ...select ... From  source_tab where ...(CTAS)”这种SQL语句,用户并没有对source_tab做任何更新操作,但MySQL对这种SQL语句做了特别处理。先来看如下表的例子。
                   CTAS操作给原表加锁例子
session_1
session_2
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from target_tab;
Empty set (0.00 sec)
mysql> select * from source_tab where name = '1';
+----+------+----+
| d1 | name | d2 |
+----+------+----+
|  4 | 1    |  1 |
|  5 | 1    |  1 |
|  6 | 1    |  1 |
|  7 | 1    |  1 |
|  8 | 1    |  1 |
+----+------+----+
5 rows in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from target_tab;
Empty set (0.00 sec)
mysql> select * from source_tab where name = '1';
+----+------+----+
| d1 | name | d2 |
+----+------+----+
|  4 | 1    |  1 |
|  5 | 1    |  1 |
|  6 | 1    |  1 |
|  7 | 1    |  1 |
|  8 | 1    |  1 |
+----+------+----+
5 rows in set (0.00 sec)
mysql> insert into target_tab select d1,name from source_tab where name = '1';
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0


mysql> update source_tab set name = '1' where name = '8';
等待
commit;


返回结果
commit;
在上面的例子中,只是简单地读 source_tab表的数据,相当于执行一个普通的SELECT语句,用一致性读就可以了。ORACLE正是这么做的,它通过MVCC技术实现的多版本数据来实现一致性读,不需要给source_tab加任何锁。我们知道InnoDB也实现了多版本数据,对普通的SELECT一致性读,也不需要加任何锁;但这里InnoDB却给source_tab加了共享锁,并没有使用多版本数据一致性读技术!
MySQL为什么要这么做呢?其原因还是为了保证恢复和复制的正确性。因为不加锁的话,如果在上述语句执行过程中,其他事务对source_tab做了更新操作,就可能导致数据恢复的结果错误。为了演示这一点,我们再重复一下前面的例子,不同的是在session_1执行事务前,先将系统变量 innodb_locks_unsafe_for_binlog的值设置为“on”(其默认值为off),具体结果如下表所示。
                  CTAS操作不给原表加锁带来的安全问题例子
session_1
session_2
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql>set innodb_locks_unsafe_for_binlog='on'
Query OK, 0 rows affected (0.00 sec)
mysql> select * from target_tab;
Empty set (0.00 sec)
mysql> select * from source_tab where name = '1';
+----+------+----+
| d1 | name | d2 |
+----+------+----+
|  4 | 1    |  1 |
|  5 | 1    |  1 |
|  6 | 1    |  1 |
|  7 | 1    |  1 |
|  8 | 1    |  1 |
+----+------+----+
5 rows in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from target_tab;
Empty set (0.00 sec)
mysql> select * from source_tab where name = '1';
+----+------+----+
| d1 | name | d2 |
+----+------+----+
|  4 | 1    |  1 |
|  5 | 1    |  1 |
|  6 | 1    |  1 |
|  7 | 1    |  1 |
|  8 | 1    |  1 |
+----+------+----+
5 rows in set (0.00 sec)
mysql> insert into target_tab select d1,name from source_tab where name = '1';
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0


session_1未提交,可以对session_1的select的记录进行更新操作。
mysql> update source_tab set name = '8' where name = '1';
Query OK, 5 rows affected (0.00 sec)
Rows matched: 5  Changed: 5  Warnings: 0
mysql> select * from source_tab where name = '8';
+----+------+----+
| d1 | name | d2 |
+----+------+----+
|  4 | 8    |  1 |
|  5 | 8    |  1 |
|  6 | 8    |  1 |
|  7 | 8    |  1 |
|  8 | 8    |  1 |
+----+------+----+
5 rows in set (0.00 sec)

更新操作先提交
mysql> commit;
Query OK, 0 rows affected (0.05 sec)
插入操作后提交
mysql> commit;
Query OK, 0 rows affected (0.07 sec)

此时查看数据,target_tab中可以插入source_tab更新前的结果,这符合应用逻辑:
mysql> select * from source_tab where name = '8';
+----+------+----+
| d1 | name | d2 |
+----+------+----+
|  4 | 8    |  1 |
|  5 | 8    |  1 |
|  6 | 8    |  1 |
|  7 | 8    |  1 |
|  8 | 8    |  1 |
+----+------+----+
5 rows in set (0.00 sec)
mysql> select * from target_tab;
+------+------+
| id   | name |
+------+------+
| 4    | 1.00 |
| 5    | 1.00 |
| 6    | 1.00 |
| 7    | 1.00 |
| 8    | 1.00 |
+------+------+
5 rows in set (0.00 sec)
mysql> select * from tt1 where name = '1';
Empty set (0.00 sec)
mysql> select * from source_tab where name = '8';
+----+------+----+
| d1 | name | d2 |
+----+------+----+
|  4 | 8    |  1 |
|  5 | 8    |  1 |
|  6 | 8    |  1 |
|  7 | 8    |  1 |
|  8 | 8    |  1 |
+----+------+----+
5 rows in set (0.00 sec)
mysql> select * from target_tab;
+------+------+
| id   | name |
+------+------+
| 4    | 1.00 |
| 5    | 1.00 |
| 6    | 1.00 |
| 7    | 1.00 |
| 8    | 1.00 |
+------+------+
5 rows in set (0.00 sec)
从上可见,设置系统变量innodb_locks_unsafe_for_binlog的值为“on”后,InnoDB不再对source_tab加锁,结果也符合应用逻辑,但是如果分析BINLOG的内容:
......
SET TIMESTAMP=1169175130;
BEGIN;
# at 274
#070119 10:51:57 server id 1  end_log_pos 105   Query   thread_id=1     exec_time=0     error_code=0
SET TIMESTAMP=1169175117;
update source_tab set name = '8' where name = '1';
# at 379
#070119 10:52:10 server id 1  end_log_pos 406   Xid = 5
COMMIT;
# at 406
#070119 10:52:14 server id 1  end_log_pos 474   Query   thread_id=2     exec_time=0     error_code=0
SET TIMESTAMP=1169175134;
BEGIN;
# at 474
#070119 10:51:29 server id 1  end_log_pos 119   Query   thread_id=2     exec_time=0     error_code=0
SET TIMESTAMP=1169175089;
insert into target_tab select d1,name from source_tab where name = '1';
# at 593
#070119 10:52:14 server id 1  end_log_pos 620   Xid = 7
COMMIT;
......
    可以发现,在BINLOG中,更新操作的位置在INSERT...SELECT之前,如果使用这个BINLOG进行数据库恢复,恢复的结果与实际的应用逻辑不符;如果进行复制,就会导致主从数据库不一致!
通过上面的例子,我们就不难理解为什么MySQL在处理“Insert  into target_tab select * from source_tab where ...”和“create  table new_tab ...select ... From  source_tab where ...”时要给source_tab加锁,而不是使用对并发影响最小的多版本数据来实现一致性读。还要特别说明的是,如果上述语句的SELECT是范围条件,InnoDB还会给源表加间隙锁(Next-Lock)。
因此,INSERT...SELECT...和 CREATE TABLE...SELECT...语句,可能会阻止对源表的并发更新,造成对源表锁的等待。如果查询比较复杂的话,会造成严重的性能问题,我们在应用中应尽量避免使用。实际上,MySQL将这种SQL叫作不确定(non-deterministic)的SQL,不推荐使用。
如果应用中一定要用这种SQL来实现业务逻辑,又不希望对源表的并发更新产生影响,可以采取以下两种措施:
一是采取上面示例中的做法,将innodb_locks_unsafe_for_binlog的值设置为“on”,强制MySQL使用多版本数据一致性读。但付出的代价是可能无法用binlog正确地恢复或复制数据,因此,不推荐使用这种方式。
二是通过使用“select * from source_tab ... Into outfile”和“load data infile ...”语句组合来间接实现,采用这种方式MySQL不会给source_tab加锁。
InnoDB在不同隔离级别下的一致性读及锁的差异
前面讲过,锁和多版本数据是InnoDB实现一致性读和ISO/ANSI SQL92隔离级别的手段,因此,在不同的隔离级别下,InnoDB处理SQL时采用的一致性读策略和需要的锁是不同的。同时,数据恢复和复制机制的特点,也对一些SQL的一致性读策略和锁策略有很大影响。将这些特性归纳成如下表所示的内容,以便读者查阅。
                                          InnoDB存储引擎中不同SQL在不同隔离级别下锁比较
隔离级别
        一致性读和锁
SQL
Read Uncommited
Read Commited
Repeatable Read
Serializable
SQL
条件




select
相等
None locks
Consisten read/None lock
Consisten read/None lock
Share locks
范围
None locks
Consisten read/None lock
Consisten read/None lock
Share Next-Key
update
相等
exclusive locks
exclusive locks
exclusive locks
Exclusive locks
范围
exclusive next-key
exclusive next-key
exclusive next-key
exclusive next-key
Insert
N/A
exclusive locks
exclusive locks
exclusive locks
exclusive locks
replace
无键冲突
exclusive locks
exclusive locks
exclusive locks
exclusive locks
键冲突
exclusive next-key
exclusive next-key
exclusive next-key
exclusive next-key
delete
相等
exclusive locks
exclusive locks
exclusive locks
exclusive locks
范围
exclusive next-key
exclusive next-key
exclusive next-key
exclusive next-key
Select ... from ... Lock in share mode
相等
Share locks
Share locks
Share locks
Share locks
范围
Share locks
Share locks
Share Next-Key
Share Next-Key
Select * from ... For update
相等
exclusive locks
exclusive locks
exclusive locks
exclusive locks
范围
exclusive locks
Share locks
exclusive next-key
exclusive next-key
Insert into ... Select ...
(指源表锁)
innodb_locks_unsafe_for_binlog=off
Share Next-Key
Share Next-Key
Share Next-Key
Share Next-Key
innodb_locks_unsafe_for_binlog=on
None locks
Consisten read/None lock
Consisten read/None lock
Share Next-Key
create table ... Select ...
(指源表锁)
innodb_locks_unsafe_for_binlog=off
Share Next-Key
Share Next-Key
Share Next-Key
Share Next-Key
innodb_locks_unsafe_for_binlog=on
None locks
Consisten read/None lock
Consisten read/None lock
Share Next-Key
从上表可以看出:对于许多SQL,隔离级别越高,InnoDB给记录集加的锁就越严格(尤其是使用范围条件的时候),产生锁冲突的可能性也就越高,从而对并发性事务处理性能的影响也就越大。因此,我们在应用中,应该尽量使用较低的隔离级别,以减少锁争用的机率。实际上,通过优化事务逻辑,大部分应用使用Read Commited隔离级别就足够了。对于一些确实需要更高隔离级别的事务,可以通过在程序中执行SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ或SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE动态改变隔离级别的方式满足需求。

什么时候使用表锁
对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用表级锁。
第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。
当然,应用中这两种事务不能太多,否则,就应该考虑使用MyISAM表了。
在InnoDB下,使用表锁要注意以下两点。
(1)使用LOCK TABLES虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层──MySQL Server负责的,仅当autocommit=0、innodb_table_locks=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server也才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁;否则,InnoDB将无法自动检测并处理这种死锁。有关死锁,下一小节还会继续讨论。
(2)在用 LOCK TABLES对InnoDB表加锁时要注意,要将AUTOCOMMIT设为0,否则MySQL不会给表加锁;事务结束前,不要用UNLOCK TABLES释放表锁,因为UNLOCK TABLES会隐含地提交事务;COMMIT或ROLLBACK并不能释放用LOCK TABLES加的表级锁,必须用UNLOCK TABLES释放表锁。正确的方式见如下语句:
例如,如果需要写表t1并从表t读,可以按如下做:
SET AUTOCOMMIT=0;
LOCK TABLES t1 WRITE, t2 READ, ...;
[do something with tables t1 and t2 here];
COMMIT;
UNLOCK TABLES;

关于死锁
上文讲过,MyISAM表锁是deadlock free的,这是因为MyISAM总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,这就决定了在InnoDB中发生死锁是可能的。如下所示的就是一个发生死锁的例子。
 InnoDB存储引擎中的死锁例子
session_1
session_2
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from table_1 where where id=1 for update;
...
做一些其他处理...
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from table_2 where id=1 for update;
...
select * from table_2 where id =1 for update;
因session_2已取得排他锁,等待
做一些其他处理...

mysql> select * from table_1 where where id=1 for update;
死锁
在上面的例子中,两个事务都需要获得对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。
发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数 innodb_lock_wait_timeout来解决。需要说明的是,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。
通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。下面就通过实例来介绍几种避免死锁的常用方法。
(1)在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。在下面的例子中,由于两个session访问两个表的顺序不同,发生死锁的机会就非常高!但如果以相同的顺序来访问,死锁就可以避免。
       InnoDB存储引擎中表顺序造成的死锁例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select first_name,last_name from actor where actor_id = 1 for update;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| PENELOPE   | GUINESS   |
+------------+-----------+
1 row in set (0.00 sec)


mysql> insert into country (country_id,country) values(110,'Test');
Query OK, 1 row affected (0.00 sec)
mysql>  insert into country (country_id,country) values(110,'Test');
等待


mysql> select first_name,last_name from actor where actor_id = 1 for update;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| PENELOPE   | GUINESS   |
+------------+-----------+
1 row in set (0.00 sec)
mysql>  insert into country (country_id,country) values(110,'Test');
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

(2)在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。
       InnoDB存储引擎中表数据操作顺序不一致造成的死锁例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select first_name,last_name from actor where actor_id = 1 for update;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| PENELOPE   | GUINESS   |
+------------+-----------+
1 row in set (0.00 sec)


mysql> select first_name,last_name from actor where actor_id = 3 for update;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| ED         | CHASE     |
+------------+-----------+
1 row in set (0.00 sec)
mysql> select first_name,last_name from actor where actor_id = 3 for update;
等待


mysql> select first_name,last_name from actor where actor_id = 1 for update;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
mysql> select first_name,last_name from actor where actor_id = 3 for update;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| ED         | CHASE     |
+------------+-----------+
1 row in set (4.71 sec)

(3)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。
       (4)前面讲过,在REPEATABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT...FOR UPDATE加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题,如下所示。
  InnoDB存储引擎中隔离级别引起的死锁例子1
session_1
session_2
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
当前session对不存在的记录加for update的锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 201 for update;
Empty set (0.00 sec)


其他session也可以对不存在的记录加for update的锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 201 for update;
Empty set (0.00 sec)
因为其他session也对该记录加了锁,所以当前的插入会等待:
mysql> insert into actor (actor_id , first_name , last_name) values(201,'Lisa','Tom');
等待


因为其他session已经对记录进行了更新,这时候再插入记录就会提示死锁并退出:
mysql> insert into actor (actor_id, first_name , last_name) values(201,'Lisa','Tom');
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
由于其他session已经退出,当前session可以获得锁并成功插入记录:
mysql> insert into actor (actor_id , first_name , last_name) values(201,'Lisa','Tom');
Query OK, 1 row affected (13.35 sec)

(5)当隔离级别为READ COMMITTED时,如果两个线程都先执行SELECT...FOR UPDATE,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁!这时如果有第3个线程又来申请排他锁,也会出现死锁。
对于这种情况,可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行ROLLBACK释放获得的排他锁,如下所示。
   InnoDB存储引擎中隔离级别引起的死锁例子2
session_1
session_2
session_3
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.01 sec)
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.01 sec)
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.01 sec)
Session_1获得for update的共享锁:
mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;
Empty set (0.00 sec)
由于记录不存在,session_2也可以获得for update的共享锁:
mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;
Empty set (0.00 sec)

Session_1可以成功插入记录:
mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom');
Query OK, 1 row affected (0.00 sec)



Session_2插入申请等待获得锁:
mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom');
等待

Session_1成功提交:
mysql> commit;
Query OK, 0 rows affected (0.04 sec)



Session_2获得锁,发现插入记录主键重,这个时候抛出了异常,但是并没有释放共享锁:
mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom');
ERROR 1062 (23000): Duplicate entry '201' for key 'PRIMARY'



Session_3申请获得共享锁,因为session_2已经锁定该记录,所以session_3需要等待:
mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;
等待

这个时候,如果session_2直接对记录进行更新操作,则会抛出死锁的异常:
mysql> update actor set last_name='Lan' where actor_id = 201;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction



Session_2释放锁后,session_3获得锁:
mysql> select first_name, last_name from actor where actor_id = 201 for update;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| Lisa       | Tom       |
+------------+-----------+
1 row in set (31.12 sec)
尽管通过上面介绍的设计和SQL优化等措施,可以大大减少死锁,但死锁很难完全避免。因此,在程序设计中总是捕获并处理死锁异常是一个很好的编程习惯。
如果出现死锁,可以用SHOW INNODB STATUS命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。下面是一段SHOW INNODB STATUS输出的样例:
mysql> show innodb status \G
…….
------------------------
LATEST DETECTED DEADLOCK
------------------------
070710 14:05:16
*** (1) TRANSACTION:
TRANSACTION 0 117470078, ACTIVE 117 sec, process no 1468, OS thread id 1197328736 inserting
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1216
MySQL thread id 7521657, query id 673468054 localhost root update
insert into country (country_id,country) values(110,'Test')
………
*** (2) TRANSACTION:
TRANSACTION 0 117470079, ACTIVE 39 sec, process no 1468, OS thread id 1164048736 starting index read, thread declared inside InnoDB 500
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1216, undo log entries 1
MySQL thread id 7521664, query id 673468058 localhost root statistics
select first_name,last_name from actor where actor_id = 1 for update
*** (2) HOLDS THE LOCK(S):
………
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
………
*** WE ROLL BACK TRANSACTION (1)
……


个人理解
1.事务实现有多种策略,最简单的就是依靠锁实现
2.四种隔离级别只在读数据和更新数据时的加锁策略上存在不同。
3.读未提交策略:对读取到的数据不加锁,对更新数据加行级共享锁,直到事务结束才释放。
   读已提交策略:对读取到的数据加行级共享锁,读完即释放;对更新数据加行级排它锁,直到事务结束才释放。
   可重复读策略:对读取到的数据加行级共享锁,直到事务结束才释放;对更新数据加行级排它锁,直到事务结束才释放。
   串行化策略:对读取到的数据加表级共享锁,直到事务结束;对更新数据加表级排它锁,直到事务结束才释放。

原文
地址:http://www.cnblogs.com/wajika/p/6680200.html
 我这4种隔离级别的相应原理总结如下:

READ_UNCOMMITED 的原理:

表现:

READ_COMMITED 的原理:

表现:

REPEATABLE READ 的原理:

表现:

SERIALIZABLE 的原理:

表现:
分类: mysql


indexOf实现引申出来的各种字符串匹配算法

我们在表单验证时,经常遇到字符串的包含问题,比如说邮件必须包含indexOf。我们现在说一下indexOf。这是es3.1引进的API ,与lastIndexOf是一套的。可以用于字符串与数组中。一些面试经常用问数组的indexOf是如何实现的,但鲜有问如何实现字符串的indexOf是如何实现,因为这是很难很难。要知道,我们平时业务都是与字符串与数组打交道,像数字与日期则更加专业(涉及到二进制,历法)是通过库来处理。
我们回来想一下为什么字符串的indexOf为何如此难?这涉及到前缀与后缀的问题,或更专业的说,你应该想到前缀树或后缀树。如果你连这些概念都没有,你是写不好indexOf。字符串的问题,可以简单理解为遍历,分为全部遍历或跳着查找。
我们看最简单的Brute-Force算法(又被戏称为boyfirend算法)。有两个字符串,长的称之为目标串,短的一般叫模式串。
其算法思想是从目标串的第一个字符串与模式串的第一字符串比较,如果相等,移动目标串的索引,将模式串的索引归零,让目标串的子串与模式串继续逐字比较。

Brute-Force算法


function indexOf(longStr, shortStr, pos) {

var i = pos || 0
/*------------------------------------*/
//若串S中从第pos(S的下标0<= pos <=StrLength(S))个字符起存在和串T相同的子串,则匹配成功。
//返回第一个这样的子串在串S中的下标;否则返回-1

var j = 0;
while (true) {
if (longStr[i + j] == void 0)
break
if (longStr[i + j] === shortStr[j]) {
j++; //继续比较后一个字符
if (shortStr[j] === void 0) {
return i
}
} else {
//重新开始新一轮的匹配
i++;
j = 0;
}
}

return -1; //串S中(第pos个字符起)不存在和串T相同的子串
}
console.log(indexOf('aadddaa', 'ddd'))

KMP算法

第二个是大名鼎鼎的“看毛片”算法,由Knuth,Morris,Pratt三人分别独立研究出来,其对于任何模式和目标序列,都可以在线性时间内完成匹配查找,而不会发生退化,是一个非常优秀的模式匹配算法。它的核心思想是预处理模式串,将模式串构造一个跳转表,有两种形式的跳转表,next与nextval, nextval可以基于next构建,也可以不。
下面这篇文章详KMP 的工件原理,大家有兴趣看看
http://blog.csdn.net/qq_29501587/article/details/52075200
但如何构建next,nextval呢?我搜了许多文章终于找到相关介绍,我汇总在下面的算法中了。
function getNext(str) {
// 求出每个子串的前后缀的共有长度,然后全部整体后移一位,首项为定值-1,得到next数组:
//首先可以肯定的是第一位的next值为0,第二位的next值为1,后面求解每一位的next值时,
//根据前一位的next值对应的字符与当前字符比较,相同,在前一位的next值加1,
//否则直接让它与第一个字符比较,求得共有长度
//比如说ababcabc
var next = [0] //第一个子串只有一个字母,不用比较,没有公共部分,为0
for (var i = 1, n = str.length; i < n; i++) {
var c = str[i]
var index = next[i - 1]
if (str[index] === c) { // a, a
next[i] = index + 1
} else {
next[i] = str[0] === c ? 1 : 0 //第一次比较a, b
}
}
// [0, 0, 1, 2, 0, 1, 2, 0]
next.unshift(-1)
next.pop();
// -1, 0 , 0, 1,2 ,0,1,2
return next
}

function getNextVal(str) {
var next = getNext(str)
//我们令 nextval[0] = -1。从 nextval[1] 开始,如果某位(字符)与它 next 值指向的位(字符)相同,
//则该位的 nextval 值就是指向位的 nextval 值(nextval[i] = nextval[ next[i] ]);
//如果不同,则该位的 nextval 值就是它自己的 next 值(nextvalue[i] = next[i])。
var nextval = [-1]
for (var i = 0, n = str.length; i < n; i++) {
if (str[i] === str[next[i]]) {
nextval[i] = nextval[next[i]]
} else {
nextval[i] = next[i]
}
}
return nextval
}

/** * KMP 算法分三分,第一步求next数组,第二步求nextval数组,第三步匹配 * http://blog.csdn.net/v_july_v/article/details/7041827 * * 前两步的求法 * http://blog.csdn.net/liuhuanjun222/article/details/48091547 * */
function KmpSearch(s, p) {
var i = 0;
var j = 0;
var sLen = s.length
var pLen = p.length
var next = getNextVal(p)
while (i < sLen && j < pLen) {
//①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
if (j == -1 || s[i] == p[j]) {
i++;
j++;
} else {
//②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
//next[j]即为j所对应的next值
j = next[j];
}
}
if (j == pLen)
return i - j;
else
return -1;
}
console.log(KmpSearch('abacababc', 'abab'))
你可以将这种算法看成DFA (有穷状态自动机)的一种退化写法,但非常晦涩,它是世界第一次打破字符串快速匹配的困局,启迪人们如何跳着匹配字符串了。

Boyer-Moore算法

Boyer-Moore算法是我们文本编辑器进行diff时,使用的一种高效算法,比KMP快三到四倍,思想也是预处理模式串,得到坏字符规则和好后缀规则移动的映射表,下面代码中MakeSkip是建立坏字符规则移动的映射表,MakeShift是建立好后缀规则的移动映射表。
下面是阮一峰的文章,简单介绍什么是坏字符串与好后缀,但没有如何介绍如何实现。
http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html
坏字符串还能轻松搞定,但好后缀就难了,都是n^2, n^3的复杂度,里面的循环大家估计也很难看懂。。。

function makeSkip(pattern) { //效率更高
var skip = {}
for (var n = pattern.length - 1, i = 0; n >= 0; n--, i++) {
var c = pattern[n]
if (!(c in skip)) {
skip[c] = i //最后一个字符串为0,倒二为1,倒三为2,重复跳过
}
}
return skip
}

function makeShift(pattern) {
var i, j, c, goods = []
var patternLen = pattern.length
var len = patternLen - 1
for (i = 0; i < len; ++i) {
goods[i] = patternLen
}

//初始化pattern最末元素的好后缀值
goods[len] = 1;

//此循环找出pattern中各元素的pre值,这里goods数组先当作pre数组使用
for (i = len, c = 0; i != 0; --i) {
for (j = 0; j < i; ++j) {
if (pattern.slice(i, len) === pattern.slice(j, len)) {
if (j == 0) {
c = patternLen - i;
} else {
if (pattern[i - 1] != pattern[j - 1]) {
goods[i - 1] = j - 1;
}
}
}
}
}

//根据pattern中个元素的pre值,计算goods值
for (i = 0; i < len; i++) {
if (goods[i] != patternLen) {
goods[i] = len - goods[i];
} else {
goods[i] = len - i + goods[i];

if (c != 0 && len - i >= c) {
goods[i] -= c;
}
}
}
return goods
}

function BMSearch(text, pattern) {
var i, j, m = 0
var patternLen = pattern.length
var textLen = text.length
i = j = patternLen - 1

var skip = makeSkip(pattern) //坏字符表
console.log(skip)
var goods = makeShift(pattern) //好后缀表
var matches = []
while (j < textLen) { //j 是给text使用
//发现目标传与模式传从后向前第1个不匹配的位置
while ((i != 0) && (pattern[i] == text[j])) {
--i
--j
}

//找到一个匹配的情况
var c = text[j]
if (i == 0 && pattern[i] == c) {
matches.push(j)
j += goods[0]
} else {
//坏字符表用字典构建比较合适
j += Math.max(goods[i], typeof skip[c] === 'number' ? skip[c] : patternLen)
}

i = patternLen - 1 //回到最后一位
}

return matches
}


console.log(BMSearch('HERE IS ASIMPLE EXAMPLE', 'EXAMPLE'))
对于进阶的单模式匹配算法而言,子串(前缀/后缀)的自包含,是至关重要的概念,是加速模式匹配效率的金钥匙,而将其发扬光大的无疑是KMP算法,BM算法使用后缀自包含,从>后向前匹配模式串的灵感,也源于此,只有透彻理解KMP算法,才可能透彻理解BM算法。
坏字符表,可以用于加速任何的单模式匹配算法,而不仅限于BM算法,对于KMP算法,坏字符表同样可以起到大幅增加匹配速度的效果。对于大字符集的文字,我们需要改变坏字符表>的使用思路,用字典来保存模式串中的字符的跳转步数,对于在字典中没有查到的字符,说明其不在模式串中,目标串当前字符直接滑动patlen个字符。

BMH算法

BMH 算法是在BM算法上改进而来,舍弃晦涩复杂的后好缀算法,仅考虑了“坏字符”策略。它首先比较文本指针所指字符和模式串的最后一个字符,如果相等再比较其余m一1个字符。无论文本中哪个字符造成了匹配失败,都将由文本中和模式串最后一个位置对应的字符来启发模式向右的移动。关于“坏字符”启发和“好尾缀”启发的对比,孙克雷的研究表明:“坏字符”启发在匹配过程中占主导地位的概率为94.O3 ,远远高于“好尾缀”启发。在一般情况下,BMH算法比BM有更好的性能,它简化了初始化过程,省去了计算“好尾缀”启发的移动距离,并省去了比较“坏字符”和“好尾缀”的过程。
算法思想:
  1. 搜索文本时,从后到前搜索;
  2. 如果碰到不匹配时,移动pattern,重新与text进行匹配;
关键:移动位置的计算shift_table如下图所示。
其中k为Pattern[0 ... m-2]中,使Pattern [ k ] ==Text [ i+m-1 ]的最大值;
如果没有可以匹配的字符,则使Pattern[ 0 ]==Text [ i+m ],即移动m个位置
  1. 如果与Pattern完全匹配,返回在Text中对应的位置;
  2. 如果搜索完Text仍然找不到完全匹配的位置,则返回-1,即查找失败

function BMHSearch(test, pattern) {
var n = test.length
var m = pattern.length
var shift = {}

// 模式串P中每个字母出现的最后的下标,最后一个字母除外
// 主串从不匹配最后一个字符,所需要左移的位数
for (var i = 0; i < m - 1; i++) {
shift[pattern[i]] = m - i - 1; //就是BM的坏字母表
}

// 模式串开始位置在主串的哪里
var s = 0;
// 从后往前匹配的变量
var j;
while (s <= n - m) {
j = m - 1;
// 从模式串尾部开始匹配
while (test[s + j] == pattern[j]) {
j--;
// 匹配成功
if (j < 0) {
return s;
}
}
// 找到坏字符(当前跟模式串匹配的最后一个字符)
// 在模式串中出现最后的位置(最后一位除外)
// 所需要从模式串末尾移动到该位置的步数
var c = test[s + m - 1]
s = s + (typeof shift[c] === 'number' ? shift[c] : m)
}
return -1;
}
console.log(BMHSearch('HERE IS ASIMPLE EXAMPLE', 'EXAMPLE'))
console.log(BMHSearch('missipipi', 'pip'))

Sunday算法

Sunday算法思想跟BM算法很相似,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符。如果该字符没有在匹配串中出现则直接跳过,即移动步长= 匹配串长度+1;否则,同BM算法一样其移动步长=匹配串中最右端的该字符到末尾的距离+1。
function sundaySearch(text, pattern) {
var textLen = text.length
var patternLen = pattern.length
if (textLen < patternLen)
return -1
var shift = {} //创建跳转表
for (i = 0; i < patternLen; i++) {
shift[pattern[i]] = patternLen - i
}
var pos = 0
while (pos <= (textLen - patternLen)) { //末端对齐
var i = pos,
j
for (j = 0; j < patternLen; j++, i++) {
if (text[i] !== pattern[j]) {
var c = text[pos + patternLen]
pos += typeof shift[c] === 'number' ? shift[c] : patternLen + 1
break
}
}
if (j === patternLen) {
return pos
}
}
return -1

}

console.log(sundaySearch('HERE IS ASIMPLE EXAMPLE', 'EXAMPLE'))
console.log(sundaySearch('missipipi', 'pip'))

Shift-And和Shift-OR算法

这个算法已经超出笔者的能力,只是简单给出链接
http://www.iteye.com/topic/1130001

bitmap算法思想

这个在腾讯面试题考过,但这个算法优缺点也太明显,本文也简单给出链接,供学霸们研究
http://www.tuicool.com/articles/aYfEvy
更多链接(里面有更多算法实现与复杂度介绍)
http://blog.csdn.net/airfer/article/details/8951802/
像我们这样的平常人怎么在项目用它们呢,可能在前端比较少用,但也不是没有,如富文本编辑器,日志处理,用了它们性能提升一大截。总之,不要天天做轻松的事,否则你没有进步。

原文地址:http://www.cnblogs.com/rubylouvre/p/6658625.html


字符串匹配是计算机的基本任务之一。
举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD"?
许多算法可以完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最常用的之一。它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth。
这种算法不太容易理解,网上有很多解释,但读起来都很费劲。直到读到Jake Boxer的文章,我才真正理解这种算法。下面,我用自己的语言,试图写一篇比较好懂的KMP算法解释。
1.
首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。
2.
因为B与A不匹配,搜索词再往后移。
3.
就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
4.
接着比较字符串和搜索词的下一个字符,还是相同。
5.
直到字符串有一个字符,与搜索词对应的字符不相同为止。
6.
这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。
7.
一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。
8.
怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。
9.
已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:
  移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为 6 - 2 等于4,所以将搜索词向后移动4位。
10.
因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。
11.
因为空格与A不匹配,继续后移一位。
12.
逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。
13.
逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。
14.
下面介绍《部分匹配表》是如何产生的。
首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。
15.
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,
  - "A"的前缀和后缀都为空集,共有元素的长度为0;
  - "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
  - "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
  - "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
  - "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
  - "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
  - "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
16.
"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。

原文地址:http://blog.csdn.net/qq_29501587/article/details/52075200

个人理解
(1)乐观锁和悲观锁主要用于解决并发更新记录时的丢失更新问题,换句话说乐观锁和悲观锁主要解决并发修改记录值且新值依赖于旧值的情况
(2)Serializable隔离级别事务可以防止更新丢失问题的发生,其他的三个隔离级别都有可能发生更新丢失问题,但Serializable效率太低,所以一般不使用Serializable,而是采用乐观锁和悲观锁来解决丢失更新问题
(3)乐观锁和悲观锁不是数据库中真正存在的锁,只是人们在解决更新丢失时的不同的解决方案,体现的是人们看待事务的态度
(4)悲观锁原理之一:对每个查询出来的记录都加排它锁,直到事务释放
(5)乐观锁原理之一:不使用任何锁,只是在记录中加一个版本字段,每次使用CAS方式更新数据,更新失败则需重复更新
(6)对于数据库的很多锁都有两个版本的分类:
   [1]行锁,页锁等分类方式只是针对范围进行分类
   [2]共享和互斥是针对同步策略分类
(7)原文对于悲观锁的理解是说当它修改记录才开始加锁,实际上是查询数据到就直接开始加锁,即在查询数据那一步就已经加了排它锁(通过select...forupdate的方式),如果不使用这种逻辑还是会出现更新丢失的问题
(8)Mysql的for update语法只有在该sql使用到了索引时才会使用行级锁。如果没有用到索引会使用表级锁把整张表锁住,需要注意这个问题!
(9)悲观锁和乐观锁的适用范围
  悲观锁主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中
  乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大整个系统的吞吐量。
(10)丢失更新、putIfAbsent等并发错误不应该通过数据库事务隔离级别来实现,要在应用程序当中去实现。
原文
深入理解乐观锁和悲观锁:http://blog.csdn.net/qq_35246620/article/details/69948587
数据库的锁机制中,咱们介绍过,数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。
乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实,不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像 MemCache、Hibernate、Tair 等都有类似的概念。
针对于不同的业务场景,应该选用不同的并发控制方式。因此,不要把乐观并发控制和悲观并发控制狭义的理解为 DBMS 中的概念,更不要把它们和数据库中提供的锁机制(行锁、表锁、排他锁、共享锁)混为一谈。事实上,在 DBMS 中,悲观锁正是利用数据库本身提供的锁机制来实现的。

悲观锁

在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法,它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作在某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。
悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守(悲观)态度。因此,在整个数据处理的过程中,都将数据处于锁定状态。 悲观锁的实现,往往依靠数据库提供的锁机制 (也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
在数据库中,悲观锁的实现流程如下:
期间,如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。
MySQL InnoDB 中使用悲观锁:
要使用悲观锁,咱们必须关闭 MySQL 数据库的自动提交属性,因为 MySQL 默认使用 autocommit 模式,也就是说,当你执行一个更新操作后,MySQL 会立刻将结果进行提交,set autocommit=0;
// 0.开始事务
begin; /* begin work; start transaction; (三者中任选一个就可以)*/
// 1.查询出商品信息
select status from t_goods where id=1 for update;
// 2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
// 3.修改商品 status 为 2
update t_goods set status=2;
// 4.提交事务
commit; /* commit work; */
在上面的查询语句中,咱们使用了select…for update的方式,这样就通过开启排他锁的方式实现了悲观锁。此时在t_goods表中,id1的那条数据就被咱们锁定了,其它的事务必须等本次事务提交之后才能执行。这样咱们就可以保证当前的数据不会被其它事务修改。
不过,在使用select…for update给数据加锁的时候,咱们需要注意锁的级别,MySQL InnoDB 默认行级锁。行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,而会使用表级锁把整张表锁住,这点需要咱们格外的注意。
优点与不足
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还会降低并行性。一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理这行数据。

乐观锁

在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。乐观事务控制最早是由孔祥重(H.T.Kung)教授提出。
乐观锁( Optimistic Locking ) 是相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的,实现乐观锁的方式就是记录数据版本。
数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当咱们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本表示与第一次取出来的版本标识值相等,则予以更新;否则,认为是过期数据。
实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳。
使用版本号实现乐观锁
使用版本号时,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作,并判断当前版本号是不是该数据的最新的版本号。
// 1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}
// 2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
// 3.修改商品 status 为 2
update t_goods
set status = 2,version = version + 1where id=#{id} and version=#{version};
优点与不足
乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

参考文章
深入理解乐观锁和悲观锁:http://blog.csdn.net/qq_35246620/article/details/69948587
一分钟教你知道乐观锁和悲观锁的区别:http://blog.csdn.net/hongchangfirst/article/details/26004335
事务的更新丢失:http://m.blog.csdn.net/qq_36074058/article/details/76685937
mysql悲观锁总结和实践:http://chenzhou123520.iteye.com/blog/1860954
mysql乐观锁总结和实践:http://chenzhou123520.iteye.com/blog/1863407


京东成都研究院面试记录:http://blog.csdn.net/ccityzh/article/details/77917353

【面试】京东成都研发部面试:http://blog.csdn.net/scherrer/article/details/45313719

      再看看:阿里菜鸟网络1-4面总结:https://www.jianshu.com/p/df11f0a95e6a

个人总结
1.网络I/O模型有两个判断标准:是否阻塞,同步还是异步,这两个标准是两回事。
2.以read操作为例:
  read的具体操作分为以下两个部分(可以参考:http://www.cnblogs.com/charlesblc/p/6202402.html):
  (1)内核等待数据可读(实际的数据到达是由操作系统完成的)
  (2)将内核读到的数据拷贝到进程(用户态)
  当用户进程阻塞在了等待数据可读的阶段,即为阻塞,不阻塞在该阶段即为非阻塞
  当用户进程将内核数据拷贝到进程,即为同步,当内核主动将内核数据拷贝到进程的缓冲区即为异步。
3.针对第二条不难得出结论:判断是否是block的依据是用户进程是否block在等待数据阶段,判断是同步还是异步的依据是把数据从内核态复制到用户态是内核主动还用户进程主动
4.常见IO模型有如下几种:同步阻塞模型、同步非阻塞模型、IO多路复用模型、异步非阻塞模型
  同步阻塞模型、同步非阻塞模型、IO多路复用模型均为同步模型,
  异步虽然是在第二个步骤才区分,不过只要是异步之间区分阻塞和非阻塞已经没有意义,异步肯定是非阻塞的
  IO多路复用模型有人叫它异步阻塞模型,这是错误的,它实际上是同步的(进程主动去内核取数据),在第一步是没有阻塞的,究其原理是通过select机制进行的,可以理解为一种特殊的同步非阻塞模型
5.IO多路复用的核心是Reactor模式
   异步非阻塞IO的核心是Proactor模式(核心是回调)
6.java BIO包是同步阻塞模型,NIO包是多路复用模型,NIO2(AIO)包异步非阻塞模型
7.关于非阻塞同步模型,它实际上是检测是否可读,如果不可读就会直接返回,后面的绝对不再执行,因此需要使用用户进程需要使用轮训去判断是否可读!!恼火的东西
原文
地址:http://www.cnblogs.com/Anker/p/3254269.html

网络IO之阻塞、非阻塞、同步、异步总结

1、前言
  在网络编程中,阻塞、非阻塞、同步、异步经常被提到。unix网络编程第一卷第六章专门讨论五种不同的IO模型,Stevens讲的非常详细,我记得去年看第一遍时候,似懂非懂,没有深入理解。网上有详细的分析:http://blog.csdn.net/historyasamirror/article/details/5778378。我结合网上博客和书总结一下,加以区别,加深理解。
2、数据流向
  网络IO操作实际过程涉及到内核和调用这个IO操作的进程。以read为例,read的具体操作分为以下两个部分:
  (1)内核等待数据可读
  (2)将内核读到的数据拷贝到进程
详细过程如下图所示:
3、网络IO模型详细分析
  常见的IO模型有阻塞、非阻塞、IO多路复用,异步。以一个生动形象的例子来说明这四个概念。周末我和女友去逛街,中午饿了,我们准备去吃饭。周末人多,吃饭需要排队,我和女友有以下几种方案:
  (1)我和女友点完餐后,不知道什么时候能做好,只好坐在餐厅里面等,直到做好,然后吃完才离开。
女友本想还和我一起逛街的,但是不知道饭能什么时候做好,只好和我一起在餐厅等,而不能去逛街,直到吃完饭才能去逛街,中间等待做饭的时间浪费掉了。这就是典型的阻塞。网络中IO阻塞如下图所示:
  (2)我女友不甘心白白在这等,又想去逛商场,又担心饭好了。所以我们逛一会,回来询问服务员饭好了没有,来来回回好多次,饭都还没吃都快累死了啦。这就是非阻塞。需要不断的询问,是否准备好了。网络IO非阻塞如下图所示:

  (3)与第二个方案差不多,餐厅安装了电子屏幕用来显示点餐的状态,这样我和女友逛街一会,回来就不用去询问服务员了,直接看电子屏幕就可以了。这样每个人的餐是否好了,都直接看电子屏幕就可以了,这就是典型的IO多路复用,如select、poll、epoll。网络IO具体模型如下图所示:
  (4)女友不想逛街,又餐厅太吵了,回家好好休息一下。于是我们叫外卖,打个电话点餐,然后我和女友可以在家好好休息一下,饭好了送货员送到家里来。这就是典型的异步,只需要打个电话说一下,然后可以做自己的事情,饭好了就送来了。linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。异步过程如下图所示:
4、同步与异步
  实际上同步与异步是针对应用程序与内核的交互而言的。同步过程中进程触发IO操作并等待或者轮询的去查看IO操作是否完成。异步过程中进程触发IO操作以后,直接返回,做自己的事情,IO交给内核来处理,完成后内核通知进程IO完成。同步与异步如下图所示:
5、阻塞与非阻塞
  简单理解为需要做一件事能不能立即得到返回应答,如果不能立即获得返回,需要等待,那就阻塞了,否则就可以理解为非阻塞。详细区别如下图所示:
 
参考资料:
http://www.open-open.com/doc/view/cbb2c3363c3b49ceb5812220a9c42e42
http://blog.csdn.net/historyasamirror/article/details/5778378
http://www.zhihu.com/question/19732473
http://www.ibm.com/developerworks/cn/linux/l-async/
冷静思考,勇敢面对,把握未来!


原文地址:http://www.cnblogs.com/Anker/p/3244857.html

C语言栈与调用惯例

1、前言
  最近在再看《程序员的自我修养》这本书,对程序的链接、装载与库有了更深入的认识。关于这本书的评价可以去豆瓣看看http://book.douban.com/subject/3652388/,强烈推荐给每一位程序员哈。今天看了第十章内存,主要讲的是栈和堆的管理。主要问题是:函数在栈中是如何布局的,如何通过缓冲区溢出来调用另外一个函数,即堆栈溢出攻击。
2、基本概念
  栈(stack):我第一次接触栈是从数据结构中,此时的栈是一种基本数据结构,栈的基本属性是先进后出(FILO)。
  在计算机系统中,栈是一个具有先进后出属性的动态内存区域。程序可以将数据压入栈,也可以将数据从栈顶弹出。栈的增长方向是向下增长,即由高地址向低地址方向。在i386下,esp寄存器定位栈顶,ebp寄存器定位栈底(栈指针)。esp始终指向栈顶,随着函数的执行,esp不断的变化,而ebp固定在栈底位置不变。
3、栈的作用
  用于维护函数调用的上下文,离开了栈函数调用没法实现。栈中保存了一个函数调用所需要的维护信息,通常称为堆栈帧或活动记录。
  堆栈栈包括的内容:
  (1)函数的返回地址和参数
  (2)临时变量
  (3)保存的上下文,例如函数调用前后保持不变的寄存器。
4、函数调用过程
  (1)把所有的参数压入栈
  (2)把当前指令的下一条指令的地址压入栈中(函数的返回地址)[可以实现堆栈溢出攻击]
  (3)跳转到函数体执行
   其中(2)(3)由指令call一起执行的。
例如下图所示的程序:
汇编代码如下图所示:
5、堆栈溢出攻击
  通过堆栈溢出改变函数的返回地址,调用另外一个过程。例如下面的程序:
汇编程序如下所示:
程序输出结果如下所示:
冷静思考,勇敢面对,把握未来!



原文地址:

IO多路复用之select总结

1、基本概念
  IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:
  (1)当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  (2)当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  (3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  (4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  (5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
  与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
2、select函数
  该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:
#include <sys/select.h>
#include <sys/time.h>

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值:就绪描述符的数目,超时返回0,出错返回-1
函数参数介绍如下:
(1)第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试。
因为文件描述符是从0开始的。
(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
          void FD_ZERO(fd_set *fdset);           //清空集合
          void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中
          void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除
          int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 
(3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
         struct timeval{
                   long tv_sec;   //seconds
                   long tv_usec;  //microseconds
       };
这个参数有三种可能:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
 原理图:
3、测试程序
  写一个TCP回射程序,程序的功能是:客户端向服务器发送信息,服务器接收并原样发送给客户端,客户端显示出接收到的信息。
服务端程序如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <errno.h>
5 #include <netinet/in.h>
6 #include <sys/socket.h>
7 #include <sys/select.h>
8 #include <sys/types.h>
9 #include <netinet/in.h>
10 #include <arpa/inet.h>
11 #include <unistd.h>
12 #include <assert.h>
13
14 #define IPADDR "127.0.0.1"
15 #define PORT 8787
16 #define MAXLINE 1024
17 #define LISTENQ 5
18 #define SIZE 10
19
20 typedef struct server_context_st
21 {
22 int cli_cnt; /*客户端个数*/ 23 int clifds[SIZE]; /*客户端的个数*/ 24 fd_set allfds; /*句柄集合*/ 25 int maxfd; /*句柄最大值*/ 26 } server_context_st;
27 static server_context_st *s_srv_ctx = NULL;
28 /*===========================================================================
29 * ==========================================================================*/ 30 static int create_server_proc(const char* ip,int port)
31 {
32 int fd;
33 struct sockaddr_in servaddr;
34 fd = socket(AF_INET, SOCK_STREAM,0);
35 if (fd == -1) {
36 fprintf(stderr, "create socket fail,erron:%d,reason:%s\n",
37 errno, strerror(errno));
38 return -1;
39 }
40
41 /*一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。*/ 42 int reuse = 1;
43 if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {
44 return -1;
45 }
46
47 bzero(&servaddr,sizeof(servaddr));
48 servaddr.sin_family = AF_INET;
49 inet_pton(AF_INET,ip,&servaddr.sin_addr);
50 servaddr.sin_port = htons(port);
51
52 if (bind(fd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1) {
53 perror("bind error: ");
54 return -1;
55 }
56
57 listen(fd,LISTENQ);
58
59 return fd;
60 }
61
62 static int accept_client_proc(int srvfd)
63 {
64 struct sockaddr_in cliaddr;
65 socklen_t cliaddrlen;
66 cliaddrlen = sizeof(cliaddr);
67 int clifd = -1;
68
69 printf("accpet clint proc is called.\n");
70
71 ACCEPT:
72 clifd = accept(srvfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
73
74 if (clifd == -1) {
75 if (errno == EINTR) {
76 goto ACCEPT;
77 } else {
78 fprintf(stderr, "accept fail,error:%s\n", strerror(errno));
79 return -1;
80 }
81 }
82
83 fprintf(stdout, "accept a new client: %s:%d\n",
84 inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
85
86 //将新的连接描述符添加到数组中 87 int i = 0;
88 for (i = 0; i < SIZE; i++) {
89 if (s_srv_ctx->clifds[i] < 0) {
90 s_srv_ctx->clifds[i] = clifd;
91 s_srv_ctx->cli_cnt++;
92 break;
93 }
94 }
95
96 if (i == SIZE) {
97 fprintf(stderr,"too many clients.\n");
98 return -1;
99 }
100101 }
102
103 static int handle_client_msg(int fd, char *buf)
104 {
105 assert(buf);
106 printf("recv buf is :%s\n", buf);
107 write(fd, buf, strlen(buf) +1);
108 return 0;
109 }
110
111 static void recv_client_msg(fd_set *readfds)
112 {
113 int i = 0, n = 0;
114 int clifd;
115 char buf[MAXLINE] = {0};
116 for (i = 0;i <= s_srv_ctx->cli_cnt;i++) {
117 clifd = s_srv_ctx->clifds[i];
118 if (clifd < 0) {
119 continue;
120 }
121 /*判断客户端套接字是否有数据*/122 if (FD_ISSET(clifd, readfds)) {
123 //接收客户端发送的信息124 n = read(clifd, buf, MAXLINE);
125 if (n <= 0) {
126 /*n==0表示读取完成,客户都关闭套接字*/127 FD_CLR(clifd, &s_srv_ctx->allfds);
128 close(clifd);
129 s_srv_ctx->clifds[i] = -1;
130 continue;
131 }
132 handle_client_msg(clifd, buf);
133 }
134 }
135 }
136 static void handle_client_proc(int srvfd)
137 {
138 int clifd = -1;
139 int retval = 0;
140 fd_set *readfds = &s_srv_ctx->allfds;
141 struct timeval tv;
142 int i = 0;
143
144 while (1) {
145 /*每次调用select前都要重新设置文件描述符和时间,因为事件发生后,文件描述符和时间都被内核修改啦*/146 FD_ZERO(readfds);
147 /*添加监听套接字*/148 FD_SET(srvfd, readfds);
149 s_srv_ctx->maxfd = srvfd;
150
151 tv.tv_sec = 30;
152 tv.tv_usec = 0;
153 /*添加客户端套接字*/154 for (i = 0; i < s_srv_ctx->cli_cnt; i++) {
155 clifd = s_srv_ctx->clifds[i];
156 /*去除无效的客户端句柄*/157 if (clifd != -1) {
158 FD_SET(clifd, readfds);
159 }
160 s_srv_ctx->maxfd = (clifd > s_srv_ctx->maxfd ? clifd : s_srv_ctx->maxfd);
161 }
162
163 /*开始轮询接收处理服务端和客户端套接字*/164 retval = select(s_srv_ctx->maxfd + 1, readfds, NULL, NULL, &tv);
165 if (retval == -1) {
166 fprintf(stderr, "select error:%s.\n", strerror(errno));
167 return;
168 }
169 if (retval == 0) {
170 fprintf(stdout, "select is timeout.\n");
171 continue;
172 }
173 if (FD_ISSET(srvfd, readfds)) {
174 /*监听客户端请求*/175 accept_client_proc(srvfd);
176 } else {
177 /*接受处理客户端消息*/178 recv_client_msg(readfds);
179 }
180 }
181 }
182
183 static void server_uninit()
184 {
185 if (s_srv_ctx) {
186 free(s_srv_ctx);
187 s_srv_ctx = NULL;
188 }
189 }
190
191 static int server_init()
192 {
193 s_srv_ctx = (server_context_st *)malloc(sizeof(server_context_st));
194 if (s_srv_ctx == NULL) {
195 return -1;
196 }
197
198 memset(s_srv_ctx, 0, sizeof(server_context_st));
199
200 int i = 0;
201 for (;i < SIZE; i++) {
202 s_srv_ctx->clifds[i] = -1;
203 }
204
205 return 0;
206 }
207
208 int main(int argc,char *argv[])
209 {
210 int srvfd;
211 /*初始化服务端context*/212 if (server_init() < 0) {
213 return -1;
214 }
215 /*创建服务,开始监听客户端请求*/216 srvfd = create_server_proc(IPADDR, PORT);
217 if (srvfd < 0) {
218 fprintf(stderr, "socket create or bind fail.\n");
219 goto err;
220 }
221 /*开始接收并处理客户端请求*/222 handle_client_proc(srvfd);
223 server_uninit();
224 return 0;
225 err:
226 server_uninit();
227 return -1;
228 }
客户端程序如下:
1 #include <netinet/in.h>
2 #include <sys/socket.h>
3 #include <stdio.h>
4 #include <string.h>
5 #include <stdlib.h>
6 #include <sys/select.h>
7 #include <time.h>
8 #include <unistd.h>
9 #include <sys/types.h>
10 #include <errno.h>
11
12 #define MAXLINE 1024
13 #define IPADDRESS "127.0.0.1"
14 #define SERV_PORT 8787
15
16 #define max(a,b) (a > b) ? a : b
17
18 static void handle_recv_msg(int sockfd, char *buf)
19 {
20 printf("client recv msg is:%s\n", buf);
21 sleep(5);
22 write(sockfd, buf, strlen(buf) +1);
23 }
24
25 static void handle_connection(int sockfd)
26 {
27 char sendline[MAXLINE],recvline[MAXLINE];
28 int maxfdp,stdineof;
29 fd_set readfds;
30 int n;
31 struct timeval tv;
32 int retval = 0;
33
34 while (1) {
35
36 FD_ZERO(&readfds);
37 FD_SET(sockfd,&readfds);
38 maxfdp = sockfd;
39
40 tv.tv_sec = 5;
41 tv.tv_usec = 0;
42
43 retval = select(maxfdp+1,&readfds,NULL,NULL,&tv);
44
45 if (retval == -1) {
46 return ;
47 }
48
49 if (retval == 0) {
50 printf("client timeout.\n");
51 continue;
52 }
53
54 if (FD_ISSET(sockfd, &readfds)) {
55 n = read(sockfd,recvline,MAXLINE);
56 if (n <= 0) {
57 fprintf(stderr,"client: server is closed.\n");
58 close(sockfd);
59 FD_CLR(sockfd,&readfds);
60 return;
61 }
62
63 handle_recv_msg(sockfd, recvline);
64 }
65 }
66 }
67
68 int main(int argc,char *argv[])
69 {
70 int sockfd;
71 struct sockaddr_in servaddr;
72
73 sockfd = socket(AF_INET,SOCK_STREAM,0);
74
75 bzero(&servaddr,sizeof(servaddr));
76 servaddr.sin_family = AF_INET;
77 servaddr.sin_port = htons(SERV_PORT);
78 inet_pton(AF_INET,IPADDRESS,&servaddr.sin_addr);
79
80 int retval = 0;
81 retval = connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
82 if (retval < 0) {
83 fprintf(stderr, "connect fail,error:%s\n", strerror(errno));
84 return -1;
85 }
86
87 printf("client send to server .\n");
88 write(sockfd, "hello server", 32);
89
90 handle_connection(sockfd);
91
92 return 0;
93 }
 
4、程序结果
  启动服务程序,执行三个个客户程序进行测试,结果如下图所示:
参考:
http://konglingchun.is-programmer.com/posts/12146.html
http://blog.163.com/smileface100@126/blog/static/27720874200951024532966/
备注:
(1)2016-10-23修改服务端,添加注释,非法客户端的处理。


 redis 使用场景 
自己实现mq 
自己实现平滑部署 
socket 的原理 
socket 和多线程怎么用 思想!!! (servlet也这样的)
nio原理 
反射 是啥 为啥用反射 jdbc用反射加载驱动有啥用 ,spring怎么用的反射 。

需求描述
我说说我遇到的一个技术难点是在我在老师公司开发一个企业应用平台的时候,老板要求我开发一个文件上传模块,要求支持多用户同时上传,同时上传多个文件,支持最高2G的文件上传,支持分片上传,暂时不做断点上传,但最好预留断点上传的开发空间。
客户端直接使用百度的webuploader,服务端使用tomcat+jdbc+spring 

技术难点
本需求的技术难点在于:
1.2G文件一次上传,tomcat转发到controller很慢,而且会造成巨大的压力和停顿。
2.多线程问题,多用户同时上传可以借助tomcat本身的多线程解决,但上传多个文件需要考虑多线程问题,分片上传也需要多线程问题,总体来说需要在文件与文件之间,文件的分块之间做好隔离和处理
3.还是多线程问题,关于数据库更新的一些策略,因为文件都会在数据库有记录。这儿也是由于上面的多线程问题导致的
4.分片上传,服务端如何处理
5.如何做断点续传,并留下开发空间

思考和解决过程
因为公司小,所以我经过各种资料查询、思考和推演,得出了我的方案,随后动手实施,踩了几个坑,最终完成开发任务。

解决方案
我的方案是:





改进措施
这个项目已经过去了半年,这半年我又经过了一些学习,知道了我当初的解决方案还有几点的改进措施
比如:可以使用netty定制化文件上传的http处理模块,从而能够更加高效的处理文件流

Introspector是一个类,位置在Java.bean.Introspector,这个类的用途是发现java类是否符合javaBean规范,也就是这个类是不是javabean。具体用法可以参照jdk文档;
Introspector有全局缓存,如果有的框架或者程序用到了JavaBeans Introspector了,那么就启用了一个系统级别的缓存,这个缓存会存放一些曾加载并分析过的javabean的引用,当web服务器关闭的时候,由于这个缓存中存放着这些javabean的引用,所以垃圾回收器不能对web容器中的javaBean对象进行回收,导致内存越来越大。
spring提供的org.springframework.web.util.IntrospectorCleanupListener就解决了这个问题,他会在web服务器停止的时候,清理一下这个Introspector缓存。使那些javabean能被垃圾回收器正确回收。
spring不会出现这种问题,因为spring在加载并分析完一个类之后会马上刷新JavaBeans Introspector缓存,这样就保证了spring不会出现这种内存泄漏的问题。
但是有很多程序和框架在使用了JavaBeans Introspector之后,都没有进行清理工作,比如quartz、struts;解决办法很简单,就是上面的那个配置。
用法很简单,就是在web.xml中加入:    
<listener>    
<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>    
</listener> 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/shileican/archive/2010/05/15/5595957.aspx


http://www.cnblogs.com/dreamworlds/p/5398468.html





一.读写分离的技术简介
数据库的读写分离简单理解
对于数据库服务器集群而言,针对于客户端的数据读写操作进行分离,即采用一台数据库服务器提供对数据的增加、删除、修改等写操作的服务,其余的多台数据库服务器提供数据的查询等读操作的服务,前者被称之为主服务器,后者被称之为从服务器,主服务器与从服务器一般而言是一对多的关系,从服务器的数据会自动同步主服务器数据,准许轻微延迟。
读写分离的作用
1.方便实现数据库服务器集群,而且一台高性能的服务器远比一群低性能的服务器贵得多
2.很多应用当中,数据库的读操作远比写操作要频繁,而数据库的写操作会加锁,一同数据库服务器同时支持读写,性能会下降,将读和写进行分离,可以让数据库的性能大大提高
3.读写分离同时也有负载均衡的支持,
4.从数据库会自动同步主数据库,直接支持了数据库备份,数据也更加安全
5.经过读写分离架构以后,可以放心的在从库上进行数据分析和处理操作
读写分离中的负载均衡和故障转移
1.当主数据库奔溃时,可以自动选取从数据库充当主数据库
2.数据库查询时,优先查询当前性能开销最小的从数据库(不一定)

二.读写分离的技术难点
1.主从同步如何实现
   基本各个数据库系统都提供了主从同步的支持,只需要简单配置即可搞定。更多的需求可以参考这些数据库的实现,主流实施方案是主库的所有更新操作写入二进制日志,从库负责从主库上拿二进制日志,然后本地执行,回放!
   以下是MySQL的主从同步示意图:
    
1.数据库主从同步的核心在于日志。
2.主服务器会对数据的写操作全部生成二进制日志
3.从服务器会产生一个IO线程,一个SQL线程提供读写分离的支持
4.读写分离的流程:主服务器对数据库进行写入后会生成具体的二进制日志,从服务器的I/0线程会以主动(轮询)或者被动(远程调用)的方式读取主数据库的日志,随I/0线程会将日志数据写入到从服务器的中继日志,最后SQL线程会来读取中继日志,将日志中的数据库写入信息,写到自己的数据库当中去,到此主从同步完成。
2.读写分离如何实现
   主从同步并不等同于读写分离,主从同步是读写分离的基础。读写分离的方案多种多样,主流的实现有:
3.负载均衡如何实现
   这儿主要是指从库如何均摊读请求的压力和主库的故障转移。
  主库的故障转移可以通过双活或者分布式技术来解决
   从库均摊压力,可以通过轮流读,或者定期检测从库的运行参数来做优先级排队,然后使用带优先级的随机算法来选取从库,当然,如果从库崩溃,应该及时从集群当中移除。
   上面的技术方案当中,除了多数据源方案以外,其它方案都或多或少有自己的负载均衡策略。
4.可扩展性如何实现
   低侵入性,低耦合性就有可扩展性,读写分离的几种途径,从上到下依次是从数据库-应用的中间层,应用的数据库驱动层,应用的数据访问工具层,业务层来解决,从上到下,耦合性越来越高,可扩展性越来越低。

三.读写分离的实现方案

1.云服务器厂商直接提供
   阿里云读写分离服务
2.mysql-jdbc驱动提供的读写分离API
  ReplicationDriver
3.自定义底层开发驱动(和上面是一个方法)
  准备以后搞搞底层驱动
4.数据库中间件
   sharding-jdbc
5.数据库工具层
   自定义jdbctemplate基类,曾经做过一个demo:https://github.com/FanHuaRan/Read-Write-Separation
6.业务代码显式指定(多数据源)
   中小型公司基本都是这样做的,可以借助spring来配置多数据源,@Profile

四.读写分离的延迟问题
MySQL的解决措施:
Master binlog的写入为多线程,而Slave同步的sql_thread为单线程(MySQL5.6之前),两者写入速度不一致,在高并发写入的情况下,Slave节点延迟会更大;所以读、写分离时,一般的做法是,应用程序加判断,
首先检查SLAVE节点同步位置以及状态是否同步至最新,确认其正常后,然后将查询请求发送至此节点。
方法如下:
1、请求发送过来时,首先在Master节点执行"Show master status",记录下当前的binlog以及position
2、在SLAVE节点上,执行 select master_pos_wait(binlog, pos[, timeout]), 等待同步到最新节点,然后发送查询请求



轮询调度
每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环。只有在当前任务主动放弃CPU控制权的情况下(比如任务挂起),才允许其他任务(包括高优先级的任务)控制CPU。其优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。但不利于后面的请求及时得到响应。

抢占式调度
允许高优先级的任务打断当前执行的任务,抢占CPU的控制权。这有利于后面的高优先级的任务也能及时得到响应。但实现相对较复杂且可能出现低优先级的任务长期得不到调度。

轮询式调度与抢占式调度的区别
 轮询调度优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
  抢占式调度实现相对较复杂
  轮询调度算法的原理是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环。
  抢占式任务调度允许调度程序根据某种原则去暂停某个正在执行的进程,将已分配给该进程的处理机重新分配给另一进程。抢占方式的优点是,可以防止一个长进程长时间占用处理机,能为大多数进程提供更公平的服务,特别是能满足对响应时间有着较严格要求的实时任务的需求。
  因为抢占式调度可能会暂停一些进程,需要记录进程的运行状态,较为复杂。轮询式只需要轮流分配资源,调度简单。
  简单的说:
  轮询式调度是让进程运行直到结束或阻塞的调度方式,容易实现,适合专用系统,不适合通用系统
抢占式调度是允许将逻辑上可继续运行的在运行过程暂停的调度方式 可防止单一进程长时间独占CPU,系统开销大(降低途径:硬件实现进程切换,或扩充主存以贮存大部分程序)。

个人理解
1.多线程同步临界区机制应该遵循的基本准则
  • 空闲让进:当无进程处于临界区时,表明临界资源处于空闲状态,允许一个请求进入临界区的进程立即进入临界区,以有效利用临界资源
  • 忙则等待:当已有进程处于临界区时,表明临界资源正在被访问,因而其他试图进入临界区的进程必须等待,以保证对临界资源的互斥访问
  • 有限等待:对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,以免陷入“死等”状态
  • 让权等待:当进程不能进入自己的临界区时,应立即释放处理器,以免进程陷入“忙等”状态
2.产生死锁的原因主要是:
    (1)因为系统资源不足。
    (2)进程运行推进的顺序不合适。
    (3)资源分配不当等。
3.产生死锁的四个必要条件:
    (1)互斥条件:一个资源每次只能被一个进程使用。
    (2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    (3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
    (4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
  4.解决死锁问题的重要算法:
     银行家算法:银行家算法是从当前状态出发,逐个按安全序列检查各客户谁能完成其工作,然后假定其完成工作且归还全部贷款,再进而检查下一个能完成工作的客户,......。如果所有客户都能完成工作,则找到一个安全序列,银行家才是安全的。银行家算法允许死锁必要条件中的互斥条件,占有且申请条件,不可抢占条件的存在,这样,它与预防死锁的几种方法相比较,限制条件少了,资源利用程度提高了。
     哲学家就餐问题 :系统中有N个并发进程,若规定每个进程需要申请R个某类资源,则当系统提供K=N*(R-1)+1个同类资源时,无论采用何种方式申请使用,一定不会发生死锁。
 
原文
  地址:http://blog.csdn.net/abigale1011/article/details/6450845
 一、要点提示
(1) 掌握死锁的概念和产生死锁的根本原因
(2) 理解产生死锁的必要条件--以下四个条件同时具备:互斥条件、不可抢占条件、占有且申请条件、循环等待条件
(3) 记住解决死锁的一般方法,掌握死锁的预防和死锁的避免二者的基本思想
(4) 掌握死锁的预防策略中资源有序分配策略
(5) 理解进程安全序列的概念,理解死锁与安全序列的关系
(6) 了解银行家算法
(7) 了解资源分配图
(8) 了解死锁的检测及恢复的思想
 
 二、内容简介
  在计算机系统中有很多一次只能由一个进程使用的资源,如打印机,磁带机,一个文件的I节点等。在多道程序设计环境中,若干进程往往要共享这类资源,而且一个进程所需要的资源不止一个。这样,就会出现若干进程竞争有限资源,又推进顺序不当,从而构成无限期循环等待的局面。这种状态就是死锁。系统发生死锁现象不仅浪费大量的系统资源,甚至导致整个系统崩溃,带来灾难性后果。所以,对于死锁问题在理论上和技术上都必须给予高度重视。
8.1 死锁的概念  
  死锁是进程死锁的简称,是由Dijkstra于1965年研究银行家算法时首先提出来的。它是计算机操作系统乃至并发程序设计中最难处理的问题之一。实际上,死锁问题不仅在计算机系统中存在,在我们日常生活中它也广泛存在。
1.什么是死锁
  我们先看看这样一个生活中的例子:在一条河上有一座桥,桥面较窄,只能容纳一辆汽车通过,无法让两辆汽车并行。如果有两辆汽车A和B分别由桥的两端驶上该桥,则对于A车来说,它走过桥面左面的一段路(即占有了桥的一部分资源),要想过桥还须等待B车让出右边的桥面,此时A车不能前进;对于B车来说,它走过桥面右边的一段路(即占有了桥的一部分资源),要想过桥还须等待A车让出左边的桥面,此时B车也不能前进。两边的车都不倒车,结果造成互相等待对方让出桥面,但是谁也不让路,就会无休止地等下去。这种现象就是死锁。如果把汽车比做进程,桥面作为资源,那麽上述问题就描述为:进程A占有资源R1,等待进程B占有的资源Rr;进程B占有资源Rr,等待进程A占有的资源R1。而且资源R1和Rr只允许一个进程占用,即:不允许两个进程同时占用。结果,两个进程都不能继续执行,若不采取其它措施,这种循环等待状况会无限期持续下去,就发生了进程死锁。  
  在计算机系统中,涉及软件,硬件资源都可能发生死锁。例如:系统中只有一台CD-ROM驱动器和一台打印机,某一个进程占有了CD-ROM驱动器,又申请打印机;另一进程占有了打印机,还申请CD-ROM。结果,两个进程都被阻塞,永远也不能自行解除。
  所谓死锁,是指多个进程循环等待它方占有的资源而无限期地僵持下去的局面。很显然,如果没有外力的作用,那麽死锁涉及到的各个进程都将永远处于封锁状态。从上面的例子可以看出,计算机系统产生死锁的根本原因就是资源有限且操作不当。即:一种原因是系统提供的资源太少了,远不能满足并发进程对资源的需求。这种竞争资源引起的死锁是我们要讨论的核心。例如:消息是一种临时性资源。某一时刻,进程A等待进程B发来的消息,进程B等待进程C发来的消息,而进程C又等待进程A发来的消息。消息未到,A,B,C三个进程均无法向前推进,也会发生进程通信上的死锁。另一种原因是由于进程推进顺序不合适引发的死锁。资源少也未必一定产生死锁。就如同两个人过独木桥,如果两个人都要先过,在独木桥上僵持不肯后退,必然会应竞争资源产生死锁;但是,如果两个人上桥前先看一看有无对方的人在桥上,当无对方的人在桥上时自己才上桥,那麽问题就解决了。所以,如果程序设计得不合理,造成进程推进的顺序不当,也会出现死锁。
2.产生死锁的必要条件
  从以上分析可见,如果在计算机系统中同时具备下面四个必要条件时,那麽会发生死锁。换句话说,只要下面四个条件有一个不具备,系统就不会出现死锁。
    〈1〉互斥条件。即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。这种独占资源如CD-ROM驱动器,打印机等等,必须在占有该资源的进程主动释放它之后,其它进程才能占有该资源。这是由资源本身的属性所决定的。如独木桥就是一种独占资源,两方的人不能同时过桥。
    〈2〉不可抢占条件。进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。如过独木桥的人不能强迫对方后退,也不能非法地将对方推下桥,必须是桥上的人自己过桥后空出桥面(即主动释放占有资源),对方的人才能过桥。
    〈3〉占有且申请条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。还以过独木桥为例,甲乙两人在桥上相遇。甲走过一段桥面(即占有了一些资源),还需要走其余的桥面(申请新的资源),但那部分桥面被乙占有(乙走过一段桥面)。甲过不去,前进不能,又不后退;乙也处于同样的状况。
    〈4〉循环等待条件。存在一个进程等待序列{P1,P2,...,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,......,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。就像前面的过独木桥问题,甲等待乙占有的桥面,而乙又等待甲占有的桥面,从而彼此循环等待。
  上面我们提到的这四个条件在死锁时会同时发生。也就是说,只要有一个必要条件不满足,则死锁就可以排除。
8.2 死锁的预防  
  前面介绍了死锁发生时的四个必要条件,只要破坏这四个必要条件中的任意一个条件,死锁就不会发生。这就为我们解决死锁问题提供了可能。一般地,解决死锁的方法分为死锁的预防,避免,检测与恢复三种(注意:死锁的检测与恢复是一个方法)。我们将在下面分别加以介绍。
  死锁的预防是保证系统不进入死锁状态的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
   〈1〉打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
   〈2〉打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。    
    〈3〉打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。但是,这种策略也有如下缺点:
(1)在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;
(2)资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;
(3)降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。    
 
(4)打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:
(1)限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;
(2)为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。
8.3 死锁的避免  
  上面我们讲到的死锁预防是排除死锁的静态策略,它使产生死锁的四个必要条件不能同时具备,从而对进程申请资源的活动加以限制,以保证死锁不会发生。下面我们介绍排除死锁的动态策略--死锁的避免,它不限制进程有关申请资源的命令,而是对进程所发出的每一个申请资源命令加以动态地检查,并根据检查结果决定是否进行资源分配。就是说,在资源分配过程中若预测有发生死锁的可能性,则加以避免。这种方法的关键是确定资源分配的安全性。
 
1.安全序列
  我们首先引入安全序列的定义:所谓系统是安全的,是指系统中的所有进程能够按照某一种次序分配资源,并且依次地运行完毕,这种进程序列{P1,P2,...,Pn}就是安全序列。如果存在这样一个安全序列,则系统是安全的;如果系统不存在这样一个安全序列,则系统是不安全的。
  安全序列{P1,P2,...,Pn}是这样组成的:若对于每一个进程Pi,它需要的附加资源可以被系统中当前可用资源加上所有进程Pj当前占有资源之和所满足,则{P1,P2,...,Pn}为一个安全序列,这时系统处于安全状态,不会进入死锁状态。  
  虽然存在安全序列时一定不会有死锁发生,但是系统进入不安全状态(四个死锁的必要条件同时发生)也未必会产生死锁。当然,产生死锁后,系统一定处于不安全状态。 
2.银行家算法
  这是一个著名的避免死锁的算法,是由Dijstra首先提出来并加以解决的。 
  [背景知识] 
  一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产,这就是银行家问题。这个问题同操作系统中资源分配问题十分相似:银行家就像一个操作系统,客户就像运行的进程,银行家的资金就是系统的资源。
  [问题的描述]
  一个银行家拥有一定数量的资金,有若干个客户要贷款。每个客户须在一开始就声明他所需贷款的总额。若该客户贷款总额不超过银行家的资金总数,银行家可以接收客户的要求。客户贷款是以每次一个资金单位(如1万RMB等)的方式进行的,客户在借满所需的全部单位款额之前可能会等待,但银行家须保证这种等待是有限的,可完成的。
  例如:有三个客户C1,C2,C3,向银行家借款,该银行家的资金总额为10个资金单位,其中C1客户要借9各资金单位,C2客户要借3个资金单位,C3客户要借8个资金单位,总计20个资金单位。某一时刻的状态如图所示。
  
C1 2(7)
C2 2(1)
C3 4(4)
余额2
C1 2(7)
C3 4(4)
 
余额4
C1 2(7)
余额8
 
余额10
 
    (a)
 
     (b)
 
     (c)
 
     (d)
 
                                       银行家算法示意
  对于a图的状态,按照安全序列的要求,我们选的第一个客户应满足该客户所需的贷款小于等于银行家当前所剩余的钱款,可以看出只有C2客户能被满足:C2客户需1个资金单位,小银行家手中的2个资金单位,于是银行家把1个资金单位借给C2客户,使之完成工作并归还所借的3个资金单位的钱,进入b图。同理,银行家把4个资金单位借给C3客户,使其完成工作,在c图中,只剩一个客户C1,它需7个资金单位,这时银行家有8个资金单位,所以C1也能顺利借到钱并完成工作。最后(见图d)银行家收回全部10个资金单位,保证不赔本。那麽客户序列{C1,C2,C3}就是个安全序列,按照这个序列贷款,银行家才是安全的。否则的话,若在图b状态时,银行家把手中的4个资金单位借给了C1,则出现不安全状态:这时C1,C3均不能完成工作,而银行家手中又没有钱了,系统陷入僵持局面,银行家也不能收回投资。
  综上所述,银行家算法是从当前状态出发,逐个按安全序列检查各客户谁能完成其工作,然后假定其完成工作且归还全部贷款,再进而检查下一个能完成工作的客户,......。如果所有客户都能完成工作,则找到一个安全序列,银行家才是安全的。
  从上面分析看出,银行家算法允许死锁必要条件中的互斥条件,占有且申请条件,不可抢占条件的存在,这样,它与预防死锁的几种方法相比较,限制条件少了,资源利用程度提高了。
这是该算法的优点。其缺点是:
   〈1〉这个算法要求客户数保持固定不变,这在多道程序系统中是难以做到的。   
   〈2〉这个算法保证所有客户在有限的时间内得到满足,但实时客户要求快速响应,所以要考虑这个因素。  
    〈3〉由于要寻找一个安全序列,实际上增加了系统的开销。
 
 
8.4 死锁的检测与恢复  
 
  一般来说,由于操作系统有并发,共享以及随机性等特点,通过预防和避免的手段达到排除死锁的目的是很困难的。这需要较大的系统开销,而且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采取任何限制性措施,但是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。因此,在实际的操作系统中往往采用死锁的检测与恢复方法来排除死锁。
  死锁检测与恢复是指系统设有专门的机构,当死锁发生时,该机构能够检测到死锁发生的位置和原因,并能通过外力破坏死锁发生的必要条件,从而使得并发进程从死锁状态中恢复出来。
 
  图中所示为一个小的死锁的例子。这时进程P1占有资源R1而申请资源R2,进程P2占有资源R2而申请资源R1,按循环等待条件,进程和资源形成了环路,所以系统是死锁状态。进程P1,P2是参与死锁的进程。
  下面我们再来看一看死锁检测算法。算法使用的数据结构是如下这些:      
      占有矩阵A:n*m阶,其中n表示并发进程的个数,m表示系统的各类资源的个数,这个矩阵记录了每一个进程当前占有各个资源类中资源的个数。
       申请矩阵R:n*m阶,其中n表示并发进程的个数,m表示系统的各类资源的个数,这个矩阵记录了每一个进程当前要完成工作需要申请的各个资源类中资源的个数。
       空闲向量T:记录当前m个资源类中空闲资源的个数。
       完成向量F:布尔型向量值为真(true)或假(false),记录当前n个并发进程能否进行完。为真即能进行完,为假则不能进行完。
       临时向量W:开始时W:=T。
算法步骤:
     (1)W:=T,
     对于所有的i=1,2,...,n,
     如果A[i]=0,则F[i]:=true;否则,F[i]:=false
     (2)找满足下面条件的下标i:
     F[i]:=false并且R[i]〈=W
     如果不存在满足上面的条件i,则转到步骤(4)。
     (3)W:=W+A[i]
     F[i]:=true
     转到步骤(2)
     (4)如果存在i,F[i]:=false,则系统处于死锁状态,且Pi进程参与了死锁。什麽时候进行死锁的检测取决于死锁发生的频率。如果死锁发生的频率高,那麽死锁检测的频率也要相应提高,这样一方面可以提高系统资源的利用率,一方面可以避免更多的进程卷入死锁。如果进程申请资源不能满足就立刻进行检测,那麽每当死锁形成时即能被发现,这和死锁避免的算法相近,只是系统的开销较大。为了减小死锁检测带来的系统开销,一般采取每隔一段时间进行一次死锁检测,或者在CPU的利用率降低到某一数值时,进行死锁的检测。 
2.死锁的恢复  
  一旦在死锁检测时发现了死锁,就要消除死锁,使系统从死锁状态中恢复过来。  
    (1)最简单,最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程。
    (2)撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素。 
  此外,还有进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的。


个人理解






原文
地址:http://www.cnblogs.com/chenweichu/articles/5644312.html
关于Listener详解
ServletContextListener
ServletContextListener接口能够监听ServletContext对象的生命周期,实际上就是监听Web应用的生命周期。当Servlet容器启动或终止Web应用时,会触发ServletContextEvent事件,该事件由ServletContextListener来处理。在ServletContextListener接口中定义了处理ServletContextEvent事件的两个方法:
contextInitialized (ServletContextEvent  event)
当Servlet容器启动Web应用时调用该方法。在调用完该方法之后,容器再对Filter 初始化,并且对那些在Web 应用启动时就需要被初始化的Servlet 进行初始化。用途:Web服务器启动时加载数据到内存中…………..
contextDestroyed(ServletContextEvent  event) 
当Servlet 容器终止Web 应用时调用该方法。在调用该方法之前,容器会先销毁所有的Servlet 和Filter 过滤器。
public ServletContext getServletContext();//ServletContextEvent事件:取得一个ServletContext(application)对象

HttpSessionListener
Session创建事件发生在每次一个新的session创建的时候,类似地Session失效事件发生在每次一个Session失效的时候。对Session的整体状态的监听。对每一个正在访问的用户,J2EE应用服务器会为其建立一个对应的HttpSession对象。当一个浏览器第一次访问网站的时候,J2EE应用服务器会新建一个HttpSession对象,并触发 HttpSession创建事件,如果注册了HttpSessionListener事件监听器,则会调用HttpSessionListener事件监听器的sessionCreated方法。相反,当这个浏览器访问结束超时的时候,J2EE应用服务器会销毁相应的HttpSession对象,触发 HttpSession销毁事件,同时调用所注册HttpSessionListener事件监听器的sessionDestroyed方法。
在 HttpSessionListener接口中定义了处理HttpSessionEvent 事件的两个方法:
sessionCreated(HttpSessionEvent event);//session创建
sessionDestroyed(HttpSessionEvent event);//session销毁
HttpSession getSession();//取得当前 sessionHttpSessionEvent事件
session的销毁有两种情况:
1、session超时,web.xml配置:
<session-config>
<session-timeout>120</session-timeout><!--120分钟后超时销毁-->
</session-config>
2、手工使session失效
public void invalidate();//使session失效方法。session.invalidate();

ServletRequestListener
用于对Request请求进行监听(创建、销毁)。
void requestInitialized(ServletRequestEvent sre);//request初始化
void requestDestroyed(ServletRequestEvent sre);//request销毁
//ServletRequestEvent事件:
ServletRequest getServletRequest();//取得一个ServletRequest对象
ServletContext getServletContext();//得一个ServletContext(application)对象

在web.xml中配置
Listener配置信息必须在Filter和Servlet配置之前,Listener的初始化(ServletContentListener初始化)比Servlet和Filter都优先,而销毁比Servlet和Filter都慢。
<listener>
<listener-class>com.listener.class</listener-class>
</listener>
Listener应用实例
  1、利用HttpSessionListener统计最多在线用户人数
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class HttpSessionListenerImpl implements HttpSessionListener {
public void sessionCreated(HttpSessionEvent event) {
ServletContext app = event.getSession().getServletContext();
int count = Integer.parseInt(app.getAttribute("onLineCount").toString());
count++;
app.setAttribute("onLineCount", count);
int maxOnLineCount = Integer.parseInt(app.getAttribute("maxOnLineCount").toString());
if (count > maxOnLineCount) {
//记录最多人数是多少
app.setAttribute("maxOnLineCount", count);
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//记录在那个时刻达到上限
app.setAttribute("date", df.format(new Date()));
}
}
//session注销、超时时候调用,停止tomcat不会调用
public void sessionDestroyed(HttpSessionEvent event) {
ServletContext app = event.getSession().getServletContext();
int count = Integer.parseInt(app.getAttribute("onLineCount").toString());
count--;
app.setAttribute("onLineCount", count); 

}
}
  2、Spring使用ContextLoaderListener加载ApplicationContext配置信息
  ContextLoaderListener的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,在web.xml配置这个监听器,启动容器时,就会默认执行它实现的方法。
  ContextLoaderListener如何查找ApplicationContext.xml的配置位置以及配置多个xml:如果在web.xml中不写任何参数配置信息,默认的路径是"/WEB-INF/applicationContext.xml",在WEB-INF目录下创建的xml文件的名称必须是applicationContext.xml(在MyEclipse中把xml文件放置在src目录下)。如果是要自定义文件名可以在web.xml里加入contextConfigLocation这个context参数。
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
  3、Spring使用Log4jConfigListener配置Log4j日志
  Spring使用Log4jConfigListener的好处:
动态的改变记录级别和策略,不需要重启Web应用。把log文件定在 /WEB-INF/logs/ 而不需要写绝对路径。因为系统把web目录的路径压入一个叫webapp.root的系统变量。这样写log文件路径时不用写绝对路径了。可以把log4j.properties和其他properties一起放在/WEB-INF/ ,而不是Class-Path。
设置log4jRefreshInterval时间,开一条watchdog线程每隔段时间扫描一下配置文件的变化。
<context-param>
<param-name>webAppRootKey</param-name>
<param-value>webapp.root</param-value>
<!-- 用于定位log文件输出位置在web应用根目录下,log4j配置文件中写输出位置:log4j.appender.FILE.File=${project.root}/logs/project.log -->
</context-param>
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>classpath:log4j.properties</param-value><!-- 载入log4j配置文件 -->
</context-param>
<context-param>
<param-name>log4jRefreshInterval</param-name>
<param-value>60000</param-value>
<!--Spring刷新Log4j配置文件的间隔60秒,单位为millisecond-->
</context-param>
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
  4、Spring使用IntrospectorCleanupListener清理缓存
  这个监听器的作用是在web应用关闭时刷新JDK的JavaBeans的Introspector缓存,以确保Web应用程序的类加载器以及其加载的类正确的释放资源。如果JavaBeans的Introspector已被用来分析应用程序类,系统级的Introspector缓存将持有这些类的一个硬引用。因此,这些类和Web应用程序的类加载器在Web应用程序关闭时将不会被垃圾收集器回收!而IntrospectorCleanupListener则会对其进行适当的清理,已使其能够被垃圾收集器回收。唯一能够清理Introspector的方法是刷新整个Introspector缓存,没有其他办法来确切指定应用程序所引用的类。这将删除所有其他应用程序在服务器的缓存的Introspector结果。
  在使用Spring内部的bean机制时,不需要使用此监听器,因为Spring自己的introspection results cache将会立即刷新被分析过的JavaBeans Introspector cache,而仅仅会在应用程序自己的ClassLoader里面持有一个cache。虽然Spring本身不产生泄漏,注意,即使在Spring框架的类本身驻留在一个“共同”类加载器(如系统的ClassLoader)的情况下,也仍然应该使用使用IntrospectorCleanupListener。在这种情况下,这个IntrospectorCleanupListener将会妥善清理Spring的introspection cache。
  应用程序类,几乎不需要直接使用JavaBeans Introspector,所以,通常都不是Introspector resource造成内存泄露。相反,许多库和框架,不清理Introspector,例如: Struts和Quartz。
  需要注意的是一个简单Introspector泄漏将会导致整个Web应用程序的类加载器不会被回收!这样做的结果,将会是在web应用程序关闭时,该应用程序所有的静态类资源(比如:单实例对象)都没有得到释放。而导致内存泄露的根本原因其实并不是这些未被回收的类!
  注意:IntrospectorCleanupListener应该注册为web.xml中的第一个Listener,在任何其他Listener之前注册,比如在Spring's ContextLoaderListener注册之前,才能确保IntrospectorCleanupListener在Web应用的生命周期适当时机生效。
<!-- memory clean -->
<listener>
<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
</listener>


CMS收集器回收永久代(元空间暂无研究)
关于sun的Hotspot JVM中的PermGen能否被GC的问题
首先要说明的是PermGen的作用,PermGen是在JVM启动时,类和方法的Meta信息被加载到内存,放在PermGen中。
一般来说,该PermGen是不会被GC掉的,但是也要视JDK的版本和GC的策略有所区别。
(1)、在JDK1.5的版本中,缺省的GC策略是不会对PermGen进行GC的,但是如果想要PermGen被GC,可以通过CMS策略来
实施,样例配置如下:
-server -Xms512m -Xmx512m -XX:+UseConcMarkSweepGC -XX:+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=60 -XX:NewSize=256m  -XX:MaxNewSize=256m -XX:PermSize=40m -XX:MaxPermSize=64m -XX:+HeapDumpOnOutOfMemoryError -XX:SurvivorRatio=8
POC如下图:

此处需要注意的是:使用CMS(ConcMarkSweep)策略时,必须有:-XX:+CMSPermGenSweepingEnabled 和-XX:+CMSClassUnloadingEnabled
来配合同时启用,才可以对PermGen进行GC(
实际主要参数为:-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled  -XX:+CMSPermGenSweepingEnabled
)。而且参数:-XX:+CMSPermGenSweepingEnabled在JDK1.6中是不存在的。
可以通过jconsole中的MBeans=>com.sun.management.HotSpotDiagnostic来验证JVM中的参数。
(2)、在JDK1.6的版本中,缺省的GC策略是不会对PermGen进行GC的,但是如果想要PermGen被GC,可以通过CMS策略来
实施,样例配置如下:
-server -Xms512m -Xmx512m -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:PermSize=40m -XX:MaxPermSize=64m -XX:+HeapDumpOnOutOfMemoryError
实际主要参数为:-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled
效果图如下:
 总结:CMS策略可以对PermGen进行GC,但是前提是应用程序停止后能保证其所使用的类完全达到可以被GC的条件,如果某些web应用中配置了listener的话,这些web应用通过应用服务器的web控制台停掉后,其listener并不会停掉,导致该应用所加载的类不会被卸载。因为:listener中的两个方法,contextInitialized()是在Servlet容器启动时执行,而contextDestroyed()是在Servlet容器停止时执行。因此要想让PermGen在应用服务器启动状态下被GC,需要以下两个条件:
(1)、配置CMS策略,如:
 -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled  -XX:+CMSPermGenSweepingEnabled,只是jdk1.6中不需要-XX:+CMSPermGenSweepingEnabled参数;

个人理解
1.半包和粘包现象
  • 半包:发送端发送了数量比较大的数据,接收端读取该数据时,数据分批到达,造成发送端一次发送而服务端多次读取的现象。通常和网络路由的缓存大小有关系,一个数据段大小超过缓存大小,那么网络就会拆包发送。
  • 粘包:发送端发送了多次数据,接收端一次性读取了所有数据,造成客户端多次发送而服务端一次读取的现象;通常是网络流量优化引起,即网络把多个小的数据段集满达到一定的数据量,从而减少网络链路中的传输次数。
      补充:分包不是问题,是服务端拆分粘包的处理手段。
2.半包和粘包现象是一对相反的问题:出现半包现象,那服务端就要组装数据,出现粘包现象,服务端就要拆分数据(也就是分包)。
3.半包和粘包问题的解决不能只依靠服务端,需要服务端和客户端一起定义数据的编码和解码规则才能够解决,其本质上是网络数据的编码和解码问题。
4.三个比较常用的编码和解码方案
  • 数据段定长,即每个数据都是固定字节。
  • 头部定长,其中包含数据正文的长度。
  • 分隔符,每个数据段结尾带一个特殊字符或者换行符,如#。
     头部定长最普遍,其次是数据段定长和分隔符。
5.客户端发送的每段数据我们称之为帧
6.Netty预置的服务端解码器
  • DelimiterBasedFrameDecoder:使用任何由用户提供的分隔符来提取帧的通用解码器,分隔符在构造函数中指定
  • LineBasedFrameDecoder:提取由行尾符(\n 或者\r\n)分隔的帧的解码器,这个解码器比DelimiterBasedFrameDecoder 更快。
  • FixedLengthFrameDecoder:提取定长帧,长度在构造函数中指定
  • LengthFieldBasedFrameDecoder:根据编码进帧头部中的长度值提取帧;该字段的偏移量以及长度在构造函数中指定
原文
地址:http://blog.csdn.net/sweettool/article/details/77018506
TCP作为常用的网络传输协议,数据流解析是网络应用开发人员永远绕不开的一个问题。
TCP数据传输是以无边界的数据流传输形式,所谓无边界是指数据发送端发送的字节数,在数据接收端接受时并不一定等于发送的字节数,可能会出现粘包情况。

一、TCP粘包情况:

1. 发送端发送了数量比较的数据,接收端读取数据时候数据分批到达,造成一次发送多次读取;通常网络路由的缓存大小有关系,一个数据段大小超过缓存大小,那么就要拆包发送。
2. 发送端发送了几次数据,接收端一次性读取了所有数据,造成多次发送一次读取;通常是网络流量优化,把多个小的数据段集满达到一定的数据量,从而减少网络链路中的传输次数。
TCP粘包的解决方案有很多种方法,最简单的一种就是发送的数据协议定义发送的数据包的结构:
1. 数据头:数据包的大小,固定长度。
2. 数据内容:数据内容,长度为数据头定义的长度大小。
实际操作如下:
a)发送端:先发送数据包的大小,再发送数据内容。
b)接收端:先解析本次数据包的大小N,在读取N个字节,这N个字节就是一个完整的数据内容。
具体流程如下:
实现源码
  1. /**  
  2.  * read size of len from sock into buf.  
  3.  */    
  4. bool readPack(int sock, char* buf, size_t len) {    
  5.     if (NULL == buf || len < 1) {    
  6.         return false;    
  7.     }    
  8.     memset(buf, 0, len); // only reset buffer len.    
  9.     ssize_t read_len = 0, readsum = 0;    
  10.     do {    
  11.         read_len = read(sock, buf + readsum, len - readsum);    
  12.         if (-1 == read_len) { // ignore error case    
  13.             return false;    
  14.         }    
  15.         printf("receive data: %s\n", buf + readsum);    
  16.         readsum += read_len;    
  17.     } while (readsum < len && 0 != read_len);    
  18.     return true;    
  19. }   

二、测试用例介绍

本篇提供的demo主要流程如下:
1. 客户端负责模拟发送数据,服务端负责接受数据,处理粘包问题
a)emulate_subpackage
模拟情况1,一个长数据经过多次才到达目的地,
在客户端字符串“This is a test case for client send subpackage data. data is not send complete at once.”每次只发送6个字节长度。服务端要把字符串集满才能处理数据(打印字符串)
b)emulate_adheringpackage
模拟情况2,多个数据在一次性到达目的地
在客户端将字符串“Hello I'm lucky. Nice too me you”切成三个数据段(都包含数据头和数据内容),然后一次性发送,服务端读取数据时对三个数据段逐个处理。

三、源码实现

server.cpp
  1. #include <cstdio>  
  2. #include <cstdlib>  
  3. #include <cstring>  
  4. #include <errno.h>  
  5. #include <sys/socket.h>  
  6. #include <sys/types.h>  
  7. #include <arpa/inet.h>  
  8. #include <unistd.h>  
  9.   
  10. void newclient(int sock);  
  11. bool readPack(int sock, char* buf, size_t len);  
  12. void safe_close(int &sock);  
  13.   
  14. int main(int argc, char *argv[]) {  
  15.     int sockfd = -1, newsockfd = -1;  
  16.     socklen_t c = 0;  
  17.     struct sockaddr_in serv_addr, cli_addr;  
  18.   
  19.     // Create socket  
  20.     sockfd = socket(AF_INET, SOCK_STREAM, 0);  
  21.     if (-1 == sockfd) {  
  22.         printf("new socket failed. errno: %d, error: %s\n", errno, strerror(errno));  
  23.         exit(-1);  
  24.     }  
  25.   
  26.     // Prepare the sockaddr_in structure  
  27.     serv_addr.sin_family = AF_INET;  
  28.     serv_addr.sin_addr.s_addr = INADDR_ANY;  
  29.     serv_addr.sin_port = htons(7890);  
  30.   
  31.     // bind  
  32.     if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {  
  33.         printf("bind failed. errno: %d, error: %s\n", errno, strerror(errno));  
  34.         exit(-1);  
  35.     }  
  36.   
  37.     // listen  
  38.     listen(sockfd, 5);  
  39.   
  40.     printf("listening...\n");  
  41.     // accept new connection.  
  42.     c = sizeof(struct sockaddr_in);  
  43.     int i = 0;  
  44.     while (i++ < 3) {  
  45.         printf("waiting for new socket accept.\n");  
  46.         newsockfd = accept(sockfd, (struct sockaddr*)&cli_addr, (socklen_t*)&c);  
  47.         if (newsockfd < 0) {  
  48.             printf("accept connect failed. errno: %d, error: %s\n", errno, strerror(errno));  
  49.             safe_close(sockfd);  
  50.             exit(-1);  
  51.         }  
  52.         pid_t pid = fork();  
  53.         if (0 == pid) {  
  54.             newclient(newsockfd);  
  55.             safe_close(sockfd);  
  56.             break;  
  57.         } else if (pid > 0) {  
  58.             safe_close(newsockfd);  
  59.         }  
  60.     }  
  61.     safe_close(sockfd);  
  62.     return 0;  
  63. }  
  64.   
  65. void newclient(int sock) {  
  66.     printf("newclient sock fd: %d\n", sock);  
  67.     int datasize = 0;  
  68.     const int HEAD_SIZE = 9;  
  69.     char buf[512] = {0};  
  70.     while (true) {  
  71.         memset(buf, 0, sizeof(buf));  
  72.         if (! readPack(sock, buf, HEAD_SIZE)) {  
  73.             printf("read head buffer failed.\n");  
  74.             safe_close(sock);  
  75.             return;  
  76.         }  
  77.   
  78.         datasize = atoi(buf);  
  79.         printf("data size: %s, value:%d\n", buf, datasize);  
  80.         memset(buf, 0, sizeof(buf));  
  81.         if (! readPack(sock, buf, datasize)) {  
  82.             printf("read data buffer failed\n");  
  83.             safe_close(sock);  
  84.             return;  
  85.         }  
  86.         printf("data size: %d, text: %s\n", datasize, buf);  
  87.         if (0 == strcmp(buf, "exit")) {  
  88.             break;  
  89.         }  
  90.     }  
  91.     memset(buf, 0, sizeof(buf));  
  92.     snprintf(buf, sizeof(buf), "from server read complete.");  
  93.     write(sock, buf, strlen(buf) + 1);  
  94.     printf("newclient sockfd: %d, finish.\n", sock);  
  95.     safe_close(sock);  
  96. }  
  97.   
  98. void safe_close(int &sock) {  
  99.     if (sock > 0) {  
  100.         close(sock);  
  101.         sock = -1;  
  102.     }  
  103. }  
  104.   
  105. /** 
  106.  * read size of len from sock into buf. 
  107.  */  
  108. bool readPack(int sock, char* buf, size_t len) {  
  109.     if (NULL == buf || len < 1) {  
  110.         return false;  
  111.     }  
  112.     memset(buf, 0, len); // only reset buffer len.  
  113.     ssize_t read_len = 0, readsum = 0;  
  114.     do {  
  115.         read_len = read(sock, buf + readsum, len - readsum);  
  116.         if (-1 == read_len) { // ignore error case  
  117.             return false;  
  118.         }  
  119.         printf("receive data: %s\n", buf + readsum);  
  120.         readsum += read_len;  
  121.     } while (readsum < len && 0 != read_len);  
  122.     return true;  
  123. }  
client.cpp
  1. #include <cstdio>  
  2. #include <cstdlib>  
  3. #include <cstring>  
  4. #include <time.h>  
  5. #include <errno.h>  
  6. #include <sys/socket.h>  
  7. #include <arpa/inet.h>  
  8. #include <unistd.h>  
  9.   
  10. void safe_close(int &sock);  
  11. void emulate_subpackage(int sock);  
  12. void emulate_adheringpackage(int sock);  
  13.   
  14. int main(int argc, char *argv[]) {  
  15.     char buf[128] = {0};  
  16.     int sockfd = -1;  
  17.     struct sockaddr_in serv_addr;  
  18.   
  19.     // Create sock  
  20.     sockfd = socket(AF_INET, SOCK_STREAM, 0);  
  21.     if (-1 == sockfd) {  
  22.         printf("new socket failed. errno: %d, error: %s\n", errno, strerror(errno));  
  23.         exit(-1);  
  24.     }  
  25.   
  26.     serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  
  27.     serv_addr.sin_family = AF_INET;  
  28.     serv_addr.sin_port = htons(7890);  
  29.   
  30.     // Connect to remote server  
  31.     if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {  
  32.         printf("connection failed. errno: %d, error: %s\n", errno, strerror(errno));  
  33.         exit(-1);  
  34.     }  
  35.     emulate_subpackage(sockfd);  
  36.     emulate_adheringpackage(sockfd);  
  37.   
  38.     const int HEAD_SIZE = 9;  
  39.     const char temp[] = "exit";  
  40.     memset(buf, 0, sizeof(buf));  
  41.     snprintf(buf, sizeof(buf), "%0.*zu", HEAD_SIZE - 1, sizeof(temp));  
  42.     write(sockfd, buf, HEAD_SIZE);  
  43.     write(sockfd, temp, sizeof(temp));  
  44.   
  45.     printf("send complete.\n");  
  46.     memset(buf, 0, sizeof(buf));  
  47.     read(sockfd, buf, sizeof(buf));  
  48.     printf("receive data: %s\n", buf);  
  49.     printf("client finish.\n");  
  50.   
  51.     safe_close(sockfd);  
  52.     return 0;  
  53. }  
  54.   
  55. void safe_close(int &sock) {  
  56.     if (sock > 0) {  
  57.         close(sock);  
  58.         sock = -1;  
  59.     }  
  60. }  
  61.   
  62. /** 
  63.  * emulate socket data write multi part. 
  64.  */  
  65. void emulate_subpackage(int sock) {  
  66.     printf("emulate_subpackage...\n");  
  67.     char text[] = "This is a test case for client send subpackage data. data is not send complete at once.";  
  68.     const size_t TEXTSIZE = sizeof(text);  
  69.     ssize_t len = 0;  
  70.     size_t sendsize = 0, sendsum = 0;  
  71.   
  72.     const int HEAD_SIZE = 9;  
  73.     char buf[64] = {0};  
  74.     snprintf(buf, HEAD_SIZE, "%08zu", TEXTSIZE);  
  75.     write(sock, buf, HEAD_SIZE);  
  76.     printf("send data size: %s\n", buf);  
  77.   
  78.     do {  
  79.         sendsize = 6;  
  80.         if (sendsum + sendsize > TEXTSIZE) {  
  81.             sendsize = TEXTSIZE - sendsum;  
  82.         }  
  83.         len = write(sock, text + sendsum, sendsize);  
  84.         if (-1 == len) {  
  85.             printf("send data failed. errno: %d, error: %s\n", errno, strerror(errno));  
  86.             return;  
  87.         }  
  88.         memset(buf, 0, sizeof(buf));  
  89.         snprintf(buf, len + 1, text + sendsum);  
  90.         printf("send data: %s\n", buf);  
  91.         sendsum += len;  
  92.         sleep(1);  
  93.     } while (sendsum < TEXTSIZE && 0 != len);  
  94. }  
  95.   
  96. /** 
  97.  * emualte socket data write adhering. 
  98.  */  
  99. void emulate_adheringpackage(int sock) {  
  100.     printf("emulate_adheringpackage...\n");  
  101.     const int HEAD_SIZE = 9;  
  102.     char buf[1024] = {0};  
  103.     char text[128] = {0};  
  104.     char *pstart = buf;  
  105.   
  106.     // append text  
  107.     memset(text, 0, sizeof(text));  
  108.     snprintf(text, sizeof(text), "Hello ");  
  109.     snprintf(pstart, HEAD_SIZE, "%08zu", strlen(text) + 1);  
  110.     pstart += HEAD_SIZE;  
  111.     snprintf(pstart, strlen(text) + 1, "%s", text);  
  112.     pstart += strlen(text) + 1;  
  113.   
  114.     // append text  
  115.     memset(text, 0, sizeof(text));  
  116.     snprintf(text, sizeof(text), "I'm lucky.");  
  117.     snprintf(pstart, HEAD_SIZE, "%08zu", strlen(text) + 1);  
  118.     pstart += HEAD_SIZE;  
  119.     snprintf(pstart, strlen(text) + 1, "%s", text);  
  120.     pstart += strlen(text) + 1;  
  121.   
  122.     // append text  
  123.     memset(text, 0, sizeof(text));  
  124.     snprintf(text, sizeof(text), "Nice too me you");  
  125.     snprintf(pstart, HEAD_SIZE, "%08zu", strlen(text) + 1);  
  126.     pstart += HEAD_SIZE;  
  127.     snprintf(pstart, strlen(text) + 1, "%s", text);  
  128.     pstart += strlen(text) + 1;  
  129.     write(sock, buf, pstart - buf);  
  130. }  
Makefile
  1. CC=g++  
  2. CFLAGS=-I  
  3.   
  4. all: server.o client.o  
  5.   
  6. server.o: server.cpp  
  7.     $(CC) -o server.o server.cpp  
  8.   
  9. client.o: client.cpp  
  10.     $(CC) -o client.o client.cpp  
  11.   
  12. clean:  
  13.     rm *.o  

四、测试结果

编译及运行
$ make
g++ -o server.o server.cpp
g++ -o client.o client.cpp
客户端模拟发送数据
$ ./client.o
emulate_subpackage...
send data size: 00000088
send data: This i
send data: s a te
send data: st cas
send data: e for
send data: client
send data: send
send data: subpac
send data: kage d
send data: ata. d
send data: ata is
send data: not s
send data: end co
send data: mplete
send data: at on
send data: ce.
emulate_adheringpackage...
send complete.
receive data: from server read complete.
client finish.

服务端模拟接受数据
$ ./server.o
listening...
waiting for new socket accept.
waiting for new socket accept.
newclient sock fd: 4
receive data: 00000088
data size: 00000088, value:88
receive data: This i
receive data: s a te
receive data: st cas
receive data: e for
receive data: client
receive data: send
receive data: subpac
receive data: kage d
receive data: ata. d
receive data: ata is
receive data: not s
receive data: end co
receive data: mplete
receive data: at on
receive data: ce.
data size: 88, text: This is a test case for client send subpackage data. data is not send complete at once.
receive data: 00000007
data size: 00000007, value:7
receive data: Hello
data size: 7, text: Hello
receive data: 00000011
data size: 00000011, value:11
receive data: I'm lucky.
data size: 11, text: I'm lucky.
receive data: 00000016
data size: 00000016, value:16
receive data: Nice too me you
data size: 16, text: Nice too me you
receive data: 00000005
data size: 00000005, value:5
receive data: exit
data size: 5, text: exit
newclient sockfd: 4, finish.

个人理解
1.Reactor模式叫做IO多路复用(有些人叫它异步阻塞,实际上这是错误的,IO多路复用设计上在read第二步操作是同步进行的)
   Proactor模式是异步非阻塞IO
2.read操作具体有两个步骤,第一个步骤区分是否阻塞,第二个步骤区分同步或者异步
3.判断是否是block的依据是用户进程是否block在等待数据阶段,判断是同步还是异步的依据是把数据从内核态复制到用户态是内核主动还用户进程主动

在学习这两个设计模式之前需要明白网络I/O的一个重要知识
网络IO操作实际过程涉及到内核和调用这个IO操作的进程(或线程)。
read的具体操作分为以下两个部分(可以参考:http://www.cnblogs.com/charlesblc/p/6202402.html):
  (1)内核等待数据可读(实际的数据到达是由操作系统完成的)
  (2)将内核读到的数据拷贝到进程(用户态)
write的具体操作分为两个部分(还有点疑惑)
       (1)  将进程或者线程的数据(用户态)拷贝到内核
       (2) 在拷贝的同时,操作系统内核就会发送数据,拷贝完发送差不多也发送结束

以read操作为例:当用户进程阻塞在了等待数据可读的阶段,即为阻塞,不阻塞在该阶段即为非阻塞
                           当用户进程将内核数据拷贝到进程,即为同步,当内核主动将内核数据拷贝到进程的缓冲区即为异步

原文
地址:http://www.cnblogs.com/daoluanxiaozi/p/3274925.html

个人理解
1.tcp和窗口
 TCP是面向连接的可靠传输协议,因此需要确保连接的可靠性与数据传输的有序性和可靠性,因此TCP使用窗口机制进行流量控制
 窗口就是:连接建立时,各端分配一块缓冲区用来存储接收的数据,并将缓冲区的尺寸发送给另一端,接收方发送的确认信息中包含了自己剩余的缓冲区尺寸,剩余缓冲区空间的数量叫做窗口。注意:窗口是可以变化的,会随着发送数据而减小,后面又可以继续分配,简单理解就是"我这次暂时还能接受你发送的多少数据"。

1.tcp标志位,有6种标识,每个标识都占一位,该位为1表示有效,为0表示无效:
   SYN(synchronous建立联机):在建立连接时应用,SYN=1而ACK=0表示客户端向服务端请求连接,SYN=1而ACK=1表示服务端接受到客户端的连接请求包后向客户端发送的同意连接的包,所以SYN只有在建立连接,且是三次握手中的前两次才会用到。

   ACK(acknowledgement确认):当ACK=1时,确认序号字段才有意义,表示希望对方接下来发送的包的序号(其实一般只有在第一次请求时连接时不是ACK包,因为并不知道对方的序列号起始位)

   FIN(finish结束):当FIN=1时,表示欲发送的字节串已经发完,并请求开始断开传输连接,所以只有在端口连接时才会使用

   PSH(push传送):当PSH=1时,表示恳求远地TCP将本报文段立即传送给其应用层,而不要比及全部缓存都填满了之后再向上交付。

   RST(reset重置):当RST=1时,表示呈现严重错误,必须断开连接,然后再重建传输连接。复位比特还用来拒绝一个不法的报文段或拒绝打开一个连接;

   URG(urgent紧急):当URG=1时,表示此报文应尽快传送,而不要按本来的列队次序来传送。与“紧急指针”字段共同应用,紧急指针指出在本报文段中的紧急数据的最后一个字节的序号,使接管方可以知道紧急数据共有多长;

2.两个号码:
SequenceNumber(顺序号码);占4个字节,是本报文段所发送的数据项目组第一个字节的序号。在TCP传送的数据流中,每一个字节都有一个序号。例如,一报文段的序号为300,而数据共100字节,则下一个报文段的序号就是400;和收到的对方的上一个包的AcknowledgeNumber(确认序号)相同,SequenceNumber在建立连接时会初始化
AcknowledgeNumber(确认号码):占4字节,是期望收到对方下次发送的数据的第一个字节的序号,也就是期望收到的下一个报文段的首部中的序号。
注意:这两个是动态增长变化的号码,和上面的SYN,ACK不是一回事,不过只有ACK==1时。AcknowledgeNumber才有意义

3.三次握手的过程
TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接
(1)第一次握手:建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认
(2)第二次握手:服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RCVD状态。
(3)第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。
 完成三次握手,客户端与服务器开始传送数据。
 注意:此时j是客户端A初始化的SequenceNumber,k是服务端B初始化的SequenceNumber,握手建立完成后也就是依赖着这些序列号进行发送和接收数据了。

4.四次挥手的过程
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
 TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。
(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。 客户端A进入FIN_WAIT_1阶段
(2)服务器B收到这个FIN,服务端进入CLOSE_WAIT阶段,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。客户端收到后进入FIN_WAIT_2阶段
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A。服务端进入LAST_ACK阶段,客户端收到后进入TIME_WAIT阶段。 
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。 此时tcp连接正式关闭
 简单说来是 “先关读,后关写”,一共需要四个阶段。以客户机发起关闭连接为例:
1.服务器读通道关闭
2.客户机写通道关闭
3.客户机读通道关闭
4.服务器写通道关闭

5.以客户端发起连接和关闭连接为例的状态迁移 
客户端TCP状态迁移:
CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED
服务器TCP状态迁移:
CLOSED->LISTEN->SYN收到->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED

6.以一个http请求为例,使用wareshark抓包,客户端ip192.168.43.194,服务端IP59.110.159.142,注意http协议是又客户端发起连接,服务端主动关闭连接
 
   编号 发送时间    发送方IP        接收方IP    协议类型 数据大小 重要信息参数
 1.3609 64.504580 192.168.43.194 59.110.159.142 TCP 66 51858→8080 [SYN] Seq=0 Win=8192 Len=0 MSS=1460 WS=4 SACK_PERM=1
   tcp第一次握手,客户端主动向服务端发送SYN包,seq初始化为0(可以为其它数字,看情况)

 2.3618 64.571665 59.110.159.142 192.168.43.194 TCP 66 8080→51858 [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0 MSS=1400 SACK_PERM=1 WS=128
   tcp第二次握手,服务端收到客户端的SYN包后,向客户端回发SYN+ACK包,服务端的seq初始化为0(可以为其它数字,看情况),ack等于客户端的seq+1(一般只有在建立和断开连接时才会呈现1增长,因为建立和端口连接的数据包正文应该是空的)

 3.3619 64.571823 192.168.43.194 59.110.159.142 TCP 54 51858→8080 [ACK] Seq=1 Ack=1 Win=65800 Len=0
   tcp第三次握手,客户端收到服务端的SYN+ACK包后,向服务端发送ACK包,最后的确认操作,此时自身的Seq已经+1,ack等于服务端的seq+1,此时连接成功

 4.3620 64.574438 192.168.43.194 59.110.159.142 HTTP 558 GET / HTTP/1.1 
   连接成功后,客户端才会向服务端发送应用层的http协议请求,http协议是包裹在tcp协议的数据正文里,通过tcp协议进行发送,这儿所使用的抓包工具封装了http正文向服务端传递的细节,我们可以不用去深究这个过程。

 5.3629 64.643283 59.110.159.142 192.168.43.194 TCP 54 8080→51858 [ACK] Seq=1 Ack=505 Win=30336 Len=0
   服务端收到应用层的http协议请求后,web服务器进行了相关的处理之后,会先向客户端发送一个tcp数据包,告诉客户端如果你收到了我接下来返回给你的http响应,请向我发送你的505序号的数据,证明你已经成功收到了请求。注意这儿Ack已经增加到了505,证明之前的http协议请求已经发送了504个字节的数据

 6.3630 64.650490 59.110.159.142 192.168.43.194 HTTP 675 HTTP/1.1 200   (text/html)
   服务端正式通过http协议向客户端返回http请求,也是包装在tcp当中的。

 7.3647 64.849862 192.168.43.194 59.110.159.142 TCP 54 51858→8080 [ACK] Seq=505 Ack=622 Win=65176 Len=0
   客户端成功接收到http请求会向服务端发送序号为505的ACK包,向服务端说明我已经成功接收到数据,服务端不用重传。注意这儿Ack已经增加到了622,证明之前的http协议返回已经发送了621个字节。

///////对于Http1.1协议而言,客户端与服务端建立的tcp连接是长连接,Keep-Alive,这可以保证客户端的其它需要的静态文件可以重用该连接而继续向服务端进行请求,不用再建立连接。当长连接时间过去之后,我们就可以抓取到四次挥手断开连接的数据包

 8.4139 84.687218 59.110.159.142 192.168.43.194 TCP 54 8080→51858 [FIN, ACK] Seq=622 Ack=505 Win=30336 Len=0
    服务端主动向客户端发送一个FIN+ACK的数据包,所以说http协议之下的tcp连接是由服务端主动关闭的。

 9.4140 84.687320 192.168.43.194 59.110.159.142 TCP 54 51858→8080 [ACK] Seq=505 Ack=623 Win=65176 Len=0
    客户端收到服务端的FIN+ACK数据包后,会关闭客户端的读通道,然后向服务端发送ACK包

10.4324 94.186985 192.168.43.194 59.110.159.142 TCP 54 51858→8080 [FIN, ACK] Seq=505 Ack=623 Win=65176 Len=0
    等待一段时间后,客户端会向服务端发送FIN+ACK包。

11.4326 94.246964 59.110.159.142 192.168.43.194 TCP 54 8080→51858 [ACK] Seq=623 Ack=506 Win=30336 Len=0
    服务端接受到该数据包,会立即关闭读通道,等待2个MSL后,服务端会向客户端发送ACK包,

    最后客户端收到最后的ACK包,关闭写通道,tcp连接已关闭。
    精致!

7.为什么主动关闭连接的一方在收到对方的FIN+ACK包之后不直接关闭连接?为什么还要进入TIME_WAIT阶段并等待2MSL的时间(Maximum Segment Lifetime的两倍时间)?答案如下:
1、保证TCP协议的全双工连接能够可靠关闭
2、保证这次连接的重复数据段从网络中消失

先说第一点,如果Client直接CLOSED了,那么由于IP协议的不可靠性或者是其它网络原因,导致Server没有收到Client最后回复的ACK。那么Server就会在超时之后继续发送FIN,此时由于Client已经CLOSED了,就找不到与重发的FIN对应的连接,最后Server就会收到RST而不是ACK,Server就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致TCP协议不符合可靠连接的要求。所以,Client不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。

再说第二点,如果Client直接CLOSED,然后又再向Server发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达Server,由于新连接和老连接的端口号是一样的,又因为TCP协议判断不同连接的依据是socket pair,于是,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据都从网络中消失。 

原文
建立连接: 
理解:窗口和滑动窗口
TCP的流量控制
TCP使用窗口机制进行流量控制
什么是窗口?
连接建立时,各端分配一块缓冲区用来存储接收的数据,并将缓冲区的尺寸发送给另一端
接收方发送的确认信息中包含了自己剩余的缓冲区尺寸
剩余缓冲区空间的数量叫做窗口
2. TCP的流控过程(滑动窗口)
TCP(Transmission Control Protocol) 传输控制协议
三次握手
TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接:
位码即tcp标志位,有6种标示:
SYN(synchronous建立联机)
ACK(acknowledgement 确认)
PSH(push传送)
FIN(finish结束)
RST(reset重置)
URG(urgent紧急)
Sequence number(顺序号码)
Acknowledge number(确认号码)
客户端TCP状态迁移:
CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED
服务器TCP状态迁移:
CLOSED->LISTEN->SYN收到->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED
 
各个状态的意义如下: 
LISTEN - 侦听来自远方TCP端口的连接请求; 
SYN-SENT -在发送连接请求后等待匹配的连接请求; 
SYN-RECEIVED - 在收到和发送一个连接请求后等待对连接请求的确认; 
ESTABLISHED- 代表一个打开的连接,数据可以传送给用户; 
FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
FIN-WAIT-2 - 从远程TCP等待连接中断请求; 
CLOSE-WAIT - 等待从本地用户发来的连接中断请求; 
CLOSING -等待远程TCP对连接中断的确认; 
LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认; 
TIME-WAIT -等待足够的时间以确保远程TCP接收到连接中断请求的确认; 
CLOSED - 没有任何连接状态;
 
TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接,如图1所示。
(1)第一次握手:建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。
(2)第二次握手:服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。
(3)第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。
完成三次握手,客户端与服务器开始传送数据。
确认号:其数值等于发送方的发送序号 +1(即接收方期望接收的下一个序列号)。
图1 TCP三次握手建立连接  
 
TCP的包头结构:
源端口 16位
目标端口 16位
序列号 32位
回应序号 32位
TCP头长度 4位
reserved 6位
控制代码 6位
窗口大小 16位
偏移量 16位
校验和 16位
选项  32位(可选)
这样我们得出了TCP包头的最小长度,为20字节
第一次握手:
客户端发送一个TCP的SYN标志位置1的包指明客户打算连接的服务器的端口,以及初始序号X,保存在包头的序列号(Sequence Number)字段里。

第二次握手:
服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序号(Acknowledgement Number)设置为客户的I S N加1以.即X+1。
 
第三次握手.
客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1.并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段放写ISN的+1

下面是具体的例子截图
1.此图包含两部分信息:TCP的三次握手(方框中的内容) (SYN, (SYN+ACK), ACK)
2. TCP的数据传输 ([TCP segment of a reassembled PUD])可以看出,server是将数据TCP层对消息包进行分片传输
(1)Server端收到HTTP请求如GET之后,构造响应消息,其中携带网页内容,在server端的HTTP层发送消息200 OK->server端的TCP层; 
(2)server端的TCP层对消息包进行分片传输; 
(3)client端的TCP层对接收到的各个消息包分片回送响应; 
(4)client端的TCP层每次收到一部分都会用ACK确认,之后server继续传输,client继续确认,直到完成响应消息的所有分片之后,Server发送组合HTTP响应包 200 OK,此时在client端的消息跟踪中才可以显示HTTP 200 OK的消息包
 
关闭连接:
 
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
 CP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。
(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。 
(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。 
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A。 
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。 
TCP采用四次挥手关闭连接如图2所示。
 图2  TCP四次挥手关闭连接
 
参见wireshark抓包,实测的抓包结果并没有严格按挥手时序。我估计是时间间隔太短造成。

 
深入理解TCP连接的释放: 
 
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
TCP协议的连接是全双工连接,一个TCP连接存在双向的读写通道。 
简单说来是 “先关读,后关写”,一共需要四个阶段。以客户机发起关闭连接为例:
1.服务器读通道关闭
2.客户机写通道关闭
3.客户机读通道关闭
4.服务器写通道关闭
关闭行为是在发起方数据发送完毕之后,给对方发出一个FIN(finish)数据段。直到接收到对方发送的FIN,且对方收到了接收确认ACK之后,双方的数据通信完全结束,过程中每次接收都需要返回确认数据段ACK。
详细过程:
    第一阶段   客户机发送完数据之后,向服务器发送一个FIN数据段,序列号为i
    1.服务器收到FIN(i)后,返回确认段ACK,序列号为i+1关闭服务器读通道
    2.客户机收到ACK(i+1)后,关闭客户机写通道
   (此时,客户机仍能通过读通道读取服务器的数据,服务器仍能通过写通道写数据)
    第二阶段 服务器发送完数据之后,向客户机发送一个FIN数据段,序列号为j;
    3.客户机收到FIN(j)后,返回确认段ACK,序列号为j+1关闭客户机读通道
    4.服务器收到ACK(j+1)后,关闭服务器写通道
这是标准的TCP关闭两个阶段,服务器和客户机都可以发起关闭,完全对称。
FIN标识是通过发送最后一块数据时设置的,标准的例子中,服务器还在发送数据,所以要等到发送完的时候,设置FIN(此时可称为TCP连接处于半关闭状态,因为数据仍可从被动关闭一方向主动关闭方传送)。如果在服务器收到FIN(i)时,已经没有数据需要发送,可以在返回ACK(i+1)的时候就设置FIN(j)标识,这样就相当于可以合并第二步和第三步。读《Linux网络编程》关闭TCP连接章节,作以下笔记:

TCP的TIME_WAIT和Close_Wait状态

 
面试时看到应聘者简历中写精通网络,TCP编程,我常问一个问题,TCP建立连接需要几次握手?95%以上的应聘者都能答对是3次。问TCP断开连接需要几次握手,70%的应聘者能答对是4次通讯。再问CLOSE_WAIT,TIME_WAIT是什么状态,怎么产生的,对服务有什么影响,如何消除?有一部分同学就回答不上来。不是我扣细节,而是在通讯为主的前端服务器上,必须有能力处理各种TCP状态。比如统计在本厂的一台前端机上高峰时间TCP连接的情况,统计命令:
 
 
  1. netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'  
 
 
结果:
 
除了ESTABLISHED,可以看到连接数比较多的几个状态是:FIN_WAIT1, TIME_WAIT, CLOSE_WAIT, SYN_RECV和LAST_ACK;下面的文章就这几个状态的产生条件、对系统的影响以及处理方式进行简单描述。
 

TCP状态

TCP状态如下图所示:
可能有点眼花缭乱?再看看这个时序图
 
下面看下大家一般比较关心的三种TCP状态

SYN_RECV 

服务端收到建立连接的SYN没有收到ACK包的时候处在SYN_RECV状态。有两个相关系统配置:
 

1,net.ipv4.tcp_synack_retries :INTEGER

默认值是5
对于远端的连接请求SYN,内核会发送SYN + ACK数据报,以确认收到上一个 SYN连接请求包。这是所谓的三次握手( threeway handshake)机制的第二个步骤。这里决定内核在放弃连接之前所送出的 SYN+ACK 数目。不应该大于255,默认值是5,对应于180秒左右时间。通常我们不对这个值进行修改,因为我们希望TCP连接不要因为偶尔的丢包而无法建立。

2,net.ipv4.tcp_syncookies

一般服务器都会设置net.ipv4.tcp_syncookies=1来防止SYN Flood攻击。假设一个用户向服务器发送了SYN报文后突然死机或掉线,那么服务器在发出SYN+ACK应答报文后是无法收到客户端的ACK报文的(第三次握手无法完成),这种情况下服务器端一般会重试(再次发送SYN+ACK给客户端)并等待一段时间后丢弃这个未完成的连接,这段时间的长度我们称为SYN Timeout,一般来说这个时间是分钟的数量级(大约为30秒-2分钟)。
 
这些处在SYNC_RECV的TCP连接称为半连接,并存储在内核的半连接队列中,在内核收到对端发送的ack包时会查找半连接队列,并将符合的requst_sock信息存储到完成三次握手的连接的队列中,然后删除此半连接。大量SYNC_RECV的TCP连接会导致半连接队列溢出,这样后续的连接建立请求会被内核直接丢弃,这就是SYN Flood攻击。
 
能够有效防范SYN Flood攻击的手段之一,就是SYN Cookie。SYN Cookie原理由D. J. Bernstain和 Eric Schenk发明。SYN Cookie是对TCP服务器端的三次握手协议作一些修改,专门用来防范SYN Flood攻击的一种手段。它的原理是,在TCP服务器收到TCP SYN包并返回TCP SYN+ACK包时,不分配一个专门的数据区,而是根据这个SYN包计算出一个cookie值。在收到TCP ACK包时,TCP服务器在根据那个cookie值检查这个TCP ACK包的合法性。如果合法,再分配专门的数据区进行处理未来的TCP连接。
 
观测服务上SYN_RECV连接个数为:7314,对于一个高并发连接的通讯服务器,这个数字比较正常。

CLOSE_WAIT

发起TCP连接关闭的一方称为client,被动关闭的一方称为server。被动关闭的server收到FIN后,但未发出ACK的TCP状态是CLOSE_WAIT。出现这种状况一般都是由于server端代码的问题,如果你的服务器上出现大量CLOSE_WAIT,应该要考虑检查代码。

TIME_WAIT

根据TCP协议定义的3次握手断开连接规定,发起socket主动关闭的一方 socket将进入TIME_WAIT状态。TIME_WAIT状态将持续2个MSL(Max Segment Lifetime),在Windows下默认为4分钟,即240秒。TIME_WAIT状态下的socket不能被回收使用. 具体现象是对于一个处理大量短连接的服务器,如果是由服务器主动关闭客户端的连接,将导致服务器端存在大量的处于TIME_WAIT状态的socket, 甚至比处于Established状态下的socket多的多,严重影响服务器的处理能力,甚至耗尽可用的socket,停止服务。
 
为什么需要TIME_WAIT?TIME_WAIT是TCP协议用以保证被重新分配的socket不会受到之前残留的延迟重发报文影响的机制,是必要的逻辑保证。
 
和TIME_WAIT状态有关的系统参数有一般由3个,本厂设置如下:
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
 
net.ipv4.tcp_fin_timeout,默认60s,减小fin_timeout,减少TIME_WAIT连接数量。
 
net.ipv4.tcp_tw_reuse = 1表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。


为了方便描述,我给这个TCP连接的一端起名为Client,给另外一端起名为Server。上图描述的是Client主动关闭的过程,FTP协议中就这样的。如果要描述Server主动关闭的过程,只要交换描述过程中的Server和Client就可以了,HTTP协议就是这样的。
描述过程:
Client调用close()函数,给Server发送FIN,请求关闭连接;Server收到FIN之后给Client返回确认ACK,同时关闭读通道(不清楚就去看一下shutdown和close的差别),也就是说现在不能再从这个连接上读取东西,现在read返回0。此时Server的TCP状态转化为CLOSE_WAIT状态。
Client收到对自己的FIN确认后,关闭 写通道,不再向连接中写入任何数据。
接下来Server调用close()来关闭连接,给Client发送FIN,Client收到后给Server回复ACK确认,同时Client关闭读通道,进入TIME_WAIT状态。
Server接收到Client对自己的FIN的确认ACK,关闭写通道,TCP连接转化为CLOSED,也就是关闭连接。
Client在TIME_WAIT状态下要等待最大数据段生存期的两倍,然后才进入CLOSED状态,TCP协议关闭连接过程彻底结束。
以上就是TCP协议关闭连接的过程,现在说一下TIME_WAIT状态。
从上面可以看到,主动发起关闭连接的操作的一方将达到TIME_WAIT状态,而且这个状态要保持Maximum Segment Lifetime的两倍时间。为什么要这样做而不是直接进入CLOSED状态?
原因有二:
一、保证TCP协议的全双工连接能够可靠关闭
二、保证这次连接的重复数据段从网络中消失
先说第一点,如果Client直接CLOSED了,那么由于IP协议的不可靠性或者是其它网络原因,导致Server没有收到Client最后回复的ACK。那么Server就会在超时之后继续发送FIN,此时由于Client已经CLOSED了,就找不到与重发的FIN对应的连接,最后Server就会收到RST而不是ACK,Server就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致TCP协议不符合可靠连接的要求。所以,Client不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。
再说第二点,如果Client直接CLOSED,然后又再向Server发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达Server,由于新连接和老连接的端口号是一样的,又因为TCP协议判断不同连接的依据是socket pair,于是,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据都从网络中消失。
各种协议都是前人千锤百炼后得到的标准,规范。从细节中都能感受到精巧和严谨。每次深入都有同一个感觉,精妙。
做个快乐的自己。 

参考文章
1.TCP的三次握手(建立连接)和四次挥手(关闭连接) http://www.cnblogs.com/Jessy/p/3535612.html
2.TCP协议中的三次握手和四次挥手(图解) http://blog.csdn.net/whuslei/article/details/6667471 ;


java没有提供任何机制来安全地终止线程(Thread.stop、suspend等方法已经声明废弃),但是提供了中断(interrupt),这是一种协作机制,能够使得一个线程终止另外一个线程的当前工作。
这种协作机制是必要的,如果某个任务、线程或者服务立即停止可能会造成数据处于不一致的状态,协作机制可以在需要停止时,首先清除当前执行的工作,然后再结束。任务本身的代码比发出请求的代码更知道如何执行清除工作,从而安全的“自我停止
生命周期结束的问题很复杂,行为良好的软件能够很完善地处理失败、关闭和取消等过程。
一.任务取消





二.停止基于线程的服务




三.处理非正常的线程终止























功能介绍

目前,只有MySQL 5.6版本的实例支持读写分离功能,因为该功能必须和只读实例一起使用。当您开通读写分离功能后,实例中会存在如下三类地址:
主实例和只读实例都具有独立的连接地址,当前由应用程序自行配置实例连接地址,实现数据读取和写入操作的分离。
读写分离功能是在此基础上,额外提供了一个读写分离地址,联动主实例及其下的所有只读实例,实现了自动的读写分离链路。应用程序只需连接同一个读写分离地址进行数据读取及写入操作,读写分离程序会自动将写入请求发往主实例,而将读取请求按照用户设置的权重发往各个只读实例。用户只需通过添加只读实例的个数,即可不断扩展系统的处理能力,应用程序上无需做任何修改。
应用程序通过不同类型的连接地址访问数据库的原理如下图所示:

功能优势

功能限制

常见问题

原文地址:https://www.alibabacloud.com/help/zh/doc-detail/51073.htm

http://blog.zhouhaocheng.cn/posts/34
https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-master-slave-replication-connection.html

目录
一、应用架构变迁下的Session管理
1.1 单体架构
1.2 分布式架构
1.3 微服务架构
二、微服务架构下分布式Session管理
2.1 Session存储介质
2.2 管理方案实现
三、微服务架构下分布式Session管理方案
四、总结

应用架构变迁下的Session管理
Session一词直译为“会话”,意指有始有终的一系列动作/消息。Session是Web应用蓬勃发展的产物之一,隐含有“面向连接”和“保持状态”两个含义,同时也指代Web服务器与客户端之间进行状态保持的解决方案。
Web服务器与客户端基于HTTP协议进行通信,HTTP协议本身是无状态的,即每一次请求之间都相互独立。但是随着Web应用的发展,Web服务器需要按照用户的一系列业务操作向客户端提供某些特定的、按需的内容,这就需要想办法将原本相对独立的HTTP请求进行关联。Session管理正是上述问题的解决方案,把用户的信息与状态保存在Session中,弥补了Web应用中HTTP协议的不足。
Session管理作为Web应用的重要解决方案之一,随着Web应用架构的不断变迁,对Session管理方案的要求也变得越来越高。对于不同应用架构,Session管理方案也有所不同。

1.1 单体架构
单体架构即是指把一个使用了分层架构的Web应用部署在单节点Web服务器上的架构类型。
图1  单体架构应用示意图

在这种架构中,虽然采用了分层架构,将整个应用分为表现层、业务逻辑层和数据访问层,每一层各司其职,让Web应用的各方面能力有所改善。但是因为应用是单体的,所有代码都部署在一个Web服务器上,随着应用的不断迭代,将会变得臃肿不堪,难以进行维护。
单体架构中,Session管理方案是在用户进行登录的时候将Session存放在应用服务器的内存中,由于只有一个应用服务器节点,用户的所有请求都是这个唯一节点进行响应处理,所以能够轻松的达到保持用户状态的目的。

1.2 分布式架构
随着Web应用的迭代开发,应用代码的维护难度成为了单体应用的一大瓶颈,为了突破这一瓶颈,出现了分布式架构的概念,企业开始使用分布式架构来代替原有的单体架构。



图2  分布式架构应用示意图

在分布式架构中,把原来的单体架构应用,按照不同的功能模块,拆分成若干个较小的应用,分别部署到若干个服务器上。这些服务器上的应用模块,各自提供相应的分布式服务,共同协作,为用户提供服务。
此时的服务器架构中,不再是单一的应用服务器节点,而是有多个服务器节点同时为用户提供服务。单体架构中的Session管理方案已经无法满足分布式架构的需要,用户的登录请求是由分布式架构中的一个服务器节点进行响应处理,当用户操作向其他服务器节点发送请求的时候,就会因为接受请求的节点上没有用户Session而导致操作失败,让用户重新登录。
所以,在分布式架构中,必须保证用户在一个服务器节点进行登陆后,不仅该节点需要在内存中保存用户Session,还需要让其他应用服务器节点也能共享到用户Session。分布式架构进行Session共享的常用方案有如下几种。
(1)存放在Cookie中
当用户Session中需要存放的数据很小的时候,可以选择将Session对象存放在浏览器的Cookie中来实现Session共享。该方案实现方便,但是由于Cookie的存储容量不大,所以只适用于Session数据量小的场景。
(2)Session复制
部分Web服务器能够支持Session复制功能,例如Tomcat。用户可以通过修改Web服务器的配置文件,让Web服务器进行Session复制,保持每一个服务器节点的Session数据都能达到一致。
但是这种方案的实现依赖于Web服务器,需要Web服务器有Session复制功能。当Web应用中Session数量较多的时候,每个服务器节点都需要有一部分内存用来存放Session,将会占用大量内存资源。同时大量的Session对象通过网络传输进行复制,不但占用了网络资源,还会因为复制同步出现延迟,导致程序运行错误。
(3)Session粘滞
Session粘滞是通过负载均衡器,将同一用户的请求都分发到固定的一个服务器节点上,让固定服务器进行响应处理,如此一来,只需要这个节点上保存了用户Session,即可保持用户的状态一致性。
但是Session粘滞方案依赖于负载均衡器,而且只能满足水平扩展的集群场景,无法满足进行应用分割后的分布式场景。

1.3微服务架构
Web应用持续发展,虽然进行了一定的拆分,把过去单体架构的巨石应用切割成了由若干个模块组成的分布式应用,但随着不断的迭代开发,这些模块应用依然会变成巨石应用,代码维护成本直线上升。
尽管可以再次进行应用拆分,但是随着拆分的应用增多,这些应用的编译、打包、部署和整合也成为了新的难题。在这样的一个环境之下,微服务架构开始受到广泛关注。
微服务架构即将一个应用拆分成一套小而相互关联的微服务,微服务之间通过暴露出来的API被其他微服务或系统所调用,在运行时,每个微服务实例通常是一个云虚拟机或一个Docker。众多微服务综合起来,构成了一个完整的微服务架构应用。
微服务架构中的微服务一般可以分为两类:无状态服务和有状态服务。无状态服务比如应用服务器,它们通常是不保存数据的,方便进行横向扩展;有状态服务需要进行数据存储,比如数据库服务和缓存服务。在Web应用中,Session用来存储用户的状态信息,所以Session管理也是有状态服务器的一种。
在实际情况中,一些企业在对原有应用进行微服务改造,实现应用向云平台迁移的时候,并不是一个单纯的微服务架构,而是一个使用微服务框架的微应用架构。
所谓的微应用架构,是由一个门户应用和多个微应用组成的架构体系。每个微应用都是基于微服务框架的Web应用,拥有自己的Web页面和逻辑代码。门户应用通过Web页面将所有微应用的页面整合到一起,然后展现给用户,为用户提供服务。
因为每一个微应用都具有自己的Web页面,这些Web页面都会通过浏览器客户端展现给用户,整个微应用架构可以近似地看作是一个大型的分布式应用,所以每个微应用都需要有Session对象,同时整个微应用架构中,同一用户的Session数据应该是一致的。

图3  单体架构VS微应用

与上文所述的分布式架构相比,微服务微应用架构让应用模块划分更精细,每个微应用的大小合适,方便进行维护和管理。通过使用Devops平台,可以让模块的迭代开发和版本更新变得极为便捷。
微服务框架在降低企业应用开发运维成本的同时,也为微应用之间的Session共享带来了挑战,单体应用被拆分成了十几个不同功能的微应用,分布式架构中的Session管理方案已经无法满足于架构需求。那什么样的Session管理方案才是适合微服务架构的呢,这将是后文中需要思考和探讨的。

微服务架构下分布式Session管理
在分布式架构中,Session管理方案是将用户Session存放在Web服务器内存中,然后通过Web服务器的复制能力或者负载均衡器的请求分发能力来实现Session共享。但是在微服务架构的实践中,企业对大型应用进行微服务改造,让应用向云环境迁移,通常会将应用拆分成十几个甚至数十个微应用,如果仍然使用Session复制、粘滞,不但会带来很多的不必要资源开销,还会降低整个企业应用的可用性和安全性。
因此,在微服务架构下,对Session的管理应该另辟蹊径,不再将Session对象保存在Web服务器内存中,而是在应用服务器架构中引入独立的中间存储介质,将企业应用中的Session对象进行统一管理。

图4  Session集中管理方案示意图
一个好的Session集中管理方案应该具备以下特点。
A、中间存储介质的读写速度要快。之前的Session管理方案将Session对象存放在服务器内存中,有着很高的读写速度,进行Session集中管理后将会在Session读写中引入网络传输,速度会有所降低,所以必须保证中间存储介质的读写速度。
B、中间存储介质要保证高可用。进行Session集中管理后,整个企业应用的Session都会存放在中间存储介质中,如果存储介质是不稳定的,那整个企业应用都将不稳定。
C、对Session的使用者来说,Session管理方案应该是透明的,切换成集中管理方案后用户无感知。
D、Session管理方案不该和某一Web服务器耦合,应该适用于所有常规Web服务器。

根据上述标准可以看出,Session集中管理方案的技术选型应该从Session存储介质和管理方案实现两方面考虑。
2.1 Session存储介质
作为Session对象的存储介质,对读写性能要求较高,所以应该择缓存服务器作为Session的存储介质。目前常用的缓存服务器有Memcache和Redis两种,因此笔者对这两种缓存服务器进行了比较。
由表中数据可以看出,虽然Redis的读写性能稍弱与Memcache,但是Redis支持的数据类型较多,而且支持数据持久化。
除此之外,Memcache使用Slab Allocation机制进行内存管理,其主要思想是按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的Key-Value数据记录,以完全解决内存碎片问题。同时MemCached使用最近最少使用(LRU)算法进行缓存回收,并且Memcached的LRU算法只是针对每个slab类执行,而不是针对整体。
这就意味着,如果所有Session的大小都大致相同,那么他们将会被分成两到三个slab类。所有其他大小差不多的数据也会被放入同一些slab中,将会与Session争用存储空间。一旦slab存满了,即使还有着更大空间的slab,这些存放在已满的slab中的数据还是会被回收,而不是放入更大的slab中。并且在特定slab中,相对而言最老的Session数据将会被Memcached清除,造成用户掉线。
Redis在存储回收策略上要比Memcache更适用于Session管理。Redis3.0版本之后,提供了良好的主从复制和集群能力,能够很好的保障Session数据的高可用。除此之外,Redis还有数据定期失效和订阅通知的能力,可以为Session共享共很多有力的支撑。

2.2 管理方案实现
目前常用的Session集中管理方案有两种,一种是Memcache-Tomcat-Session,另一种是Spring Session。
Memcache-Tomcat-Session是一个基于Memcache和Tomcat实现Session集中管理的开源方案。通过扩展Tomcat的SessionManager,并且在配置文件中替换Tomcat默认的SessionManager来实现Session管理。虽然实现起来比较简单,但是与Tomcat耦合,不适用于其他Web服务器。
Spring Session是Spring提供的一套Session管理方案,通过一个SessionFilter将所有请求进行拦截,然后使用Request包装类来接管Session管理。Spring Session不与Web服务器耦合,能够适用于常规的服务器。同时还提供了统一浏览器多Session等功能。
Spring Session虽然优点颇多,但是实现Session管理功能的代码量也比较大,还需要配合Spring-data-redis使用,学习成本比较大,遇到问题不好维护。

三、微服务架构下分布式Session管理方案

经过上文的分析,在微服务架构中,使用Session集中管理的方式维护微应用的Session是比较好的选择。因为在Session存储介质上,Redis要比Memcache合适,所以使用Redis来集中存放微应用Session更为合适。在Session管理实现方案上,Spring Session的实现思路适用性较高,但是代码量大,学习和维护成本高,所以笔者认为参照Spring Session的思路,自己开发一套轻量级的代码是比较好的选择。
经过设计,微服务架构中的Session集中管理示意图如下。

图5  Session集中管理方案示意图

1、参照SpringSession的实现,使用SessionFilter进行请求拦截,然后通过Request包装类接管Web服务器的Session管理。在Request包装类中,重写getSession方法,Session使用方法和过去一样,对使用者透明。
2、基于jedis开发一个分布式缓存SDK模块,用于Session共享模块和Redis中间进行通信,能够增加Session集中管理的可扩展性,如果需要支持其他的缓存服务器,对缓存SDK进行扩展开发即可。
3、搭建Redis集群用于存放微应用Session,以保证Session数据的高可用。Redis集群示意图如下。
集群中包含两个Master和两个Slave,两个Master对Session数据进行分片存储,而Slave可用于进行数据备份和读写分离。
总结

本文的主要目的在于对微服务架构下分布式Session管理方案进行探索与实践。目前正处于微服务的时代,将会有越来越多的企业开始着手于企业应用的微服务改造,Session管理作为Web应用的基石之一,必然会是企业进行微服务改造时候首先需要考虑的问题之一。随着应用架构的变迁,过去的Session管理方案,已经不能满足于微服务架构的需求,必须寻求新的可行方案。
微服务架构下,整个应用被分割成了大量的小而相互关联的微服务,Session管理需要另辟蹊径,使用集中管理方案来代替原来将Session存放在Web服务器本地缓存的方案。通过向架构中引入高性能的缓存服务器,将整个微服务架构下的Session进行统一管理。
本文中对目前常用的两个缓存服务器进行了对比,认为使用Redis进行Session的统一存储较为合适。同时文中还对常用的两种Session集中管理实现进行了比较,认为虽然Spring Session的实现方案比较好,但是学习成本较高,不便于维护,所以笔者决定参照Spring Session的实现原理,开发一套轻量级的Session管理方案。通过这套轻量级的Session管理方案,能够很好地实现微服务架构下分布式Session的集中管理,并且对于Session的使用者来说,这套方案是完全透明的。
微服务的时代已经到来,微服务架构带来了新的挑战,也带来了更好的机遇,让我们一起遇见未来。
原文地址:http://www.primeton.com/read.php?id=2244&his=1

问题分析
存在则更新,不存在则插入的并发问题在于不使用一定的策略,当多个线程同时查询到数据不存在,然后插入数据就会造成数据重复。因为是插入数据,所以无法使用行锁,故前三个隔离级别的事务都无法解决问题。
这个的问题的解决可以在设计层面、应用层面、数据库层面进行解决,总结出以下几种解决方案
解决方案
一.设计层面
  1. 不使用自增或者UUID作为主键,显示指定主键,如果一个字段不行,那就两个字段建立主键。
      这样就可以直接利用主键约束,重复插入直接失败,然后就可以更新。强烈推荐!

二.数据库层面
  1.串行化隔离级别
     在数据库层面直接串行化各个事务,串行化事务是表锁,会严重损害并行性,因此不推荐使用。
  2.与设计层面的那个解决方案是一样的,当应用可以指定主键时,使用如下几种语法(MySQL)
      INSERT ... ON DUPLICATE KEY UPDATE : 插入时如果检测到主键重复则更新,直接一条sql搞定,强烈推荐
      INSERT ... INTO ... WHERE NOT EXISTS (...)插入时再判短一次重复
      INSERT IGNORE插入时如果检测到主键重复则忽略
      REPLACE插入时如果检测到主键重复则替换,这个可能还是有问题,因为可能丢失更新,看怎么处理哇
  3.存储过程
     将整个存在则更新,不存在则插入逻辑依靠存储过程实现(中间加锁),简单方便。

三.应用层面
   1.使用分布式锁
      分布式锁包括可以依赖于数据库实现、中心缓存实现和分布式锁框架实现(zookeeper)
   3.消息队列
     实时要求不高的可以直接将处理请求加入消息队列,后续串行处理。
   4.临时缓存
     在应用当中先本地处理(或者缓存处理),这个处理过程当中保证安全,过段时间后进行数据库更新,不适合分布式应用。
参考文章
1. 查询记录是否存在,不存在即插入,存在即更新:https://my.oschina.net/xuguangwu/blog/839611
2.ON DUPLICATE KEY UPDATE重复插入时更新http://lobert.iteye.com/blog/1604122
3.How to INSERT If Row Does Not Exist (UPSERT) in MySQL:https://chartio.com/resources/tutorials/how-to-insert-if-row-does-not-exist-upsert-in-mysql/




个人总结
唯一订单号的业务需求
  • 订单号不能重复
  • 订单号没有规则,即编码规则不能加入任何和公司运营相关的数据,外部人员无法通过订单ID猜测到订单量,不能被遍历。
  • 订单号长度固定,且不能太长。
  • 易读,易沟通,不要出现数字字母换乱现象。
  • 生成耗时尽量短
解决方案
1、数据库自增长ID
  • 优势:无需编码
  • 缺陷:大表不能做水平分表,否则插入删除时容易出现问题;高并发下插入数据需要加入事务机制;在业务操作父、子表(关联表)插入时,先要插入父表,再插入子表
2、时间戳+随机数
  • 优势:编码简单
  • 缺陷:随机数存在重复问题,即使在相同的时间戳下。每次插入数据库前需要校验下是否已经存在相同的数值。
3、时间戳+会员ID
  • 优势:同一时间,一个用户不会存在两张订单
  • 缺陷:会员ID也会透露运营数据,鸡生蛋,蛋生鸡的问题
4、GUID/UUID
  • 优势:简单
  • 劣势:用户不友好,索引关联效率较低。
5、分布式ID生成系统
  • 优势:可充分满足各类需求
  • 劣势:算法较复杂
      系统方案:
      1.时间戳+机器编号+序列化,参考twitter的SnowFlake
      2.美团Leaf生成系统

原文
业务需求:
  • 订单号不能重复
  • 订单号没有规则,即编码规则不能加入任何和公司运营相关的数据,外部人员无法通过订单ID猜测到订单量。不能被遍历。
  • 订单号长度固定,且不能太长
  • 易读,易沟通,不要出现数字字母换乱现象
  • 生成耗时
关于订单号的生成,一些比较简单的方案:
1、数据库自增长ID
  • 优势:无需编码
  • 缺陷:
    • 大表不能做水平分表,否则插入删除时容易出现问题
    • 高并发下插入数据需要加入事务机制
    • 在业务操作父、子表(关联表)插入时,先要插入父表,再插入子表
2、时间戳+随机数
  • 优势:编码简单
  • 缺陷:随机数存在重复问题,即使在相同的时间戳下。每次插入数据库前需要校验下是否已经存在相同的数值。
3、时间戳+会员ID
  • 优势:同一时间,一个用户不会存在两张订单
  • 缺陷:会员ID也会透露运营数据,鸡生蛋,蛋生鸡的问题
4、GUID/UUID
  • 优势:简单
  • 劣势:用户不友好,索引关联效率较低。
今天要分享的方案:来自twitter的SnowFlake
Twitter-Snowflake算法产生的背景相当简单,为了满足Twitter每秒上万条消息的请求,每条消息都必须分配一条唯一的id,这些id还需要一些大致的顺序(方便客户端排序),并且在分布式系统中不同机器产生的id必须不同.Snowflake算法核心把时间戳,工作机器id,序列号(毫秒级时间41位+机器ID 10位+毫秒内序列12位)组合在一起。
在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。
除了最高位bit标记为不可用以外,其余三组bit占位均可浮动,看具体的业务需求而定。默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id。下文会具体分析。
Snowflake – 时间戳
这里时间戳的细度是毫秒级,具体代码如下,建议使用64位linux系统机器,因为有vdso,gettimeofday()在用户态就可以完成操作,减少了进入内核态的损耗。

1
2
3
4
5
6
uint64_t generateStamp()
{
    timeval tv;
    gettimeofday(&tv, 0);
    return (uint64_t)tv.tv_sec * 1000 + (uint64_t)tv.tv_usec / 1000;
}

默认情况下有41个bit可以供使用,那么一共有T(1llu << 41)毫秒供你使用分配,年份 = T / (3600 * 24 * 365 * 1000) = 69.7年。如果你只给时间戳分配39个bit使用,那么根据同样的算法最后年份 = 17.4年。
Snowflake – 工作机器id
严格意义上来说这个bit段的使用可以是进程级,机器级的话你可以使用MAC地址来唯一标示工作机器,工作进程级可以使用IP+Path来区分工作进程。如果工作机器比较少,可以使用配置文件来设置这个id是一个不错的选择,如果机器过多配置文件的维护是一个灾难性的事情。
这里的解决方案是需要一个工作id分配的进程,可以使用自己编写一个简单进程来记录分配id,或者利用Mysql auto_increment机制也可以达到效果。
工作进程与工作id分配器只是在工作进程启动的时候交互一次,然后工作进程可以自行将分配的id数据落文件,下一次启动直接读取文件里的id使用。这个工作机器id的bit段也可以进一步拆分,比如用前5个bit标记进程id,后5个bit标记线程id之类:D
Snowflake – 序列号
序列号就是一系列的自增id(多线程建议使用atomic),为了处理在同一毫秒内需要给多条消息分配id,若同一毫秒把序列号用完了,则“等待至下一毫秒”。

1
2
3
4
5
6
7
8
uint64_t waitNextMs(uint64_t lastStamp)
{
    uint64_t cur = 0;
    do {
        cur = generateStamp();
    } while (cur <= lastStamp);
    return cur;
}

总体来说,是一个很高效很方便的GUID产生算法,一个int64_t字段就可以胜任,不像现在主流128bit的GUID算法,即使无法保证严格的id序列性,但是对于特定的业务,比如用做游戏服务器端的GUID产生会很方便。另外,在多线程的环境下,序列号使用atomic可以在代码实现上有效减少锁的密度。
该项目地址为:https://github.com/twitter/snowflake 是用Scala实现的。核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/** Copyright 2010-2012 Twitter, Inc.*/
package com.twitter.service.snowflake
 
import com.twitter.ostrich.stats.Stats
import com.twitter.service.snowflake.gen._
import java.util.Random
import com.twitter.logging.Logger
 
/**
* An object that generates IDs.
* This is broken into a separate class in case
* we ever want to support multiple worker threads
* per process
*/
class IdWorker(val workerId: Long, val datacenterId: Long, private val reporter: Reporter, var sequence: Long = 0L)
extends Snowflake.Iface {
  private[this] def genCounter(agent: String) = {
    Stats.incr("ids_generated")
    Stats.incr("ids_generated_%s".format(agent))
  }
  private[this] val exceptionCounter = Stats.getCounter("exceptions")
  private[this] val log = Logger.get
  private[this] val rand = new Random
 
  val twepoch = 1288834974657L
 
  private[this] val workerIdBits = 5L
  private[this] val datacenterIdBits = 5L
  private[this] val maxWorkerId = -1L ^ (-1L << workerIdBits)
  private[this] val maxDatacenterId = -1L ^ (-1L << datacenterIdBits)
  private[this] val sequenceBits = 12L
 
  private[this] val workerIdShift = sequenceBits
  private[this] val datacenterIdShift = sequenceBits + workerIdBits
  private[this] val timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits
  private[this] val sequenceMask = -1L ^ (-1L << sequenceBits)
 
  private[this] var lastTimestamp = -1L
 
  // sanity check for workerId
  if (workerId > maxWorkerId || workerId < 0) {
    exceptionCounter.incr(1)
    throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0".format(maxWorkerId))
  }
 
  if (datacenterId > maxDatacenterId || datacenterId < 0) {
    exceptionCounter.incr(1)
    throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0".format(maxDatacenterId))
  }
 
  log.info("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
    timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId)
 
  def get_id(useragent: String): Long = {
    if (!validUseragent(useragent)) {
      exceptionCounter.incr(1)
      throw new InvalidUserAgentError
    }
 
    val id = nextId()
    genCounter(useragent)
 
    reporter.report(new AuditLogEntry(id, useragent, rand.nextLong))
    id
  }
 
  def get_worker_id(): Long = workerId
  def get_datacenter_id(): Long = datacenterId
  def get_timestamp() = System.currentTimeMillis
 
  protected[snowflake] def nextId(): Long = synchronized {
    var timestamp = timeGen()
 
    if (timestamp < lastTimestamp) {
      exceptionCounter.incr(1)
      log.error("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
      throw new InvalidSystemClock("Clock moved backwards.  Refusing to generate id for %d milliseconds".format(
        lastTimestamp - timestamp))
    }
 
    if (lastTimestamp == timestamp) {
      sequence = (sequence + 1) & sequenceMask
      if (sequence == 0) {
        timestamp = tilNextMillis(lastTimestamp)
      }
    } else {
      sequence = 0
    }
 
    lastTimestamp = timestamp
    ((timestamp - twepoch) << timestampLeftShift) |
      (datacenterId << datacenterIdShift) |
      (workerId << workerIdShift) |
      sequence
  }
 
  protected def tilNextMillis(lastTimestamp: Long): Long = {
    var timestamp = timeGen()
    while (timestamp <= lastTimestamp) {
      timestamp = timeGen()
    }
    timestamp
  }
 
  protected def timeGen(): Long = System.currentTimeMillis()
 
  val AgentParser = """([a-zA-Z][a-zA-Z\-0-9]*)""".r
 
  def validUseragent(useragent: String): Boolean = useragent match {
    case AgentParser(_) => true
    case _ => false
  }
}

由UC实现的JAVA版本代码(略有修改)
来源:https://github.com/sumory/uc/blob/master/src/com/sumory/uc/id/IdWorker.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package com.sumory.uc.id;
 
import java.math.BigInteger;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
/**
* 42位的时间前缀+10位的节点标识+12位的sequence避免并发的数字(12位不够用时强制得到新的时间前缀)
* <p>
* <b>对系统时间的依赖性非常强,需要关闭ntp的时间同步功能,或者当检测到ntp时间调整后,拒绝分配id。
*
* @author sumory.wu
* @date 2012-2-26 下午6:40:28
*/
public class IdWorker {
    private final static Logger logger = LoggerFactory.getLogger(IdWorker.class);
 
    private final long workerId;
    private final long snsEpoch = 1330328109047L;// 起始标记点,作为基准
    private long sequence = 0L;// 0,并发控制
    private final long workerIdBits = 10L;// 只允许workid的范围为:0-1023
    private final long maxWorkerId = -1L ^ -1L << this.workerIdBits;// 1023,1111111111,10位
    private final long sequenceBits = 12L;// sequence值控制在0-4095
 
    private final long workerIdShift = this.sequenceBits;// 12
    private final long timestampLeftShift = this.sequenceBits + this.workerIdBits;// 22
    private final long sequenceMask = -1L ^ -1L << this.sequenceBits;// 4095,111111111111,12位
 
    private long lastTimestamp = -1L;
 
    public IdWorker(long workerId) {
        super();
        if (workerId > this.maxWorkerId || workerId < 0) {// workid < 1024[10位:2的10次方]
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", this.maxWorkerId));
        }
        this.workerId = workerId;
    }
 
    public synchronized long nextId() throws Exception {
        long timestamp = this.timeGen();
        if (this.lastTimestamp == timestamp) {// 如果上一个timestamp与新产生的相等,则sequence加一(0-4095循环),下次再使用时sequence是新值
            //System.out.println("lastTimeStamp:" + lastTimestamp);
            this.sequence = this.sequence + 1 & this.sequenceMask;
            if (this.sequence == 0) {
                timestamp = this.tilNextMillis(this.lastTimestamp);// 重新生成timestamp
            }
        }
        else {
            this.sequence = 0;
        }
        if (timestamp < this.lastTimestamp) {
            logger.error(String.format("Clock moved backwards.Refusing to generate id for %d milliseconds", (this.lastTimestamp - timestamp)));
            throw new Exception(String.format("Clock moved backwards.Refusing to generate id for %d milliseconds", (this.lastTimestamp - timestamp)));
        }
 
        this.lastTimestamp = timestamp;
        // 生成的timestamp
        return timestamp - this.snsEpoch << this.timestampLeftShift | this.workerId << this.workerIdShift | this.sequence;
    }
 
    /**
     * 保证返回的毫秒数在参数之后
     *
     * @param lastTimestamp
     * @return
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }
 
    /**
     * 获得系统当前毫秒数
     *
     * @return
     */
    private long timeGen() {
        return System.currentTimeMillis();
    }
 
    public static void main(String[] args) throws Exception {
        IdWorker iw1 = new IdWorker(1);
        IdWorker iw2 = new IdWorker(2);
        IdWorker iw3 = new IdWorker(3);
 
        // System.out.println(iw1.maxWorkerId);
        // System.out.println(iw1.sequenceMask);
 
        System.out.println("---------------------------");
 
        long workerId = 1L;
        long twepoch = 1330328109047L;
        long sequence = 0L;// 0
        long workerIdBits = 10L;
        long maxWorkerId = -1L ^ -1L << workerIdBits;// 1023,1111111111,10位
        long sequenceBits = 12L;
 
        long workerIdShift = sequenceBits;// 12
        long timestampLeftShift = sequenceBits + workerIdBits;// 22
        long sequenceMask = -1L ^ -1L << sequenceBits;// 4095,111111111111,12位
 
        long ct = System.currentTimeMillis();// 1330328109047L;//
        System.out.println(ct);
 
        System.out.println(ct - twepoch);
        System.out.println(ct - twepoch << timestampLeftShift);// 左移22位:*2的22次方
        System.out.println(workerId << workerIdShift);// 左移12位:*2的12次方
        System.out.println("哈哈");
        System.out.println(ct - twepoch << timestampLeftShift | workerId << workerIdShift);
        long result = ct - twepoch << timestampLeftShift | workerId << workerIdShift | sequence;// 取时间的低40位 | (小于1024:只有12位)的低12位 | 计算的sequence
        System.out.println(result);
 
        System.out.println("---------------");
        for (int i = 0; i < 10; i++) {
            System.out.println(iw1.nextId());
        }
 
        Long t1 = 66708908575965184l;
        Long t2 = 66712718304231424l;
        Long t3 = 66715908575739904l;
        Long t4 = 66717361423925248l;
        System.out.println(Long.toBinaryString(t1));
        System.out.println(Long.toBinaryString(t2));
        System.out.println(Long.toBinaryString(t3));
        System.out.println(Long.toBinaryString(t4));
        //1110110011111111011001100001111100 0001100100 000000000000
        //1110110100000010110111010010010010 0001100100 000000000000
        //1110110100000101110000111110111110 0001100100 000000000000
        //1110110100000111000101100011010000 0001100100 000000000000
        System.out.println(Long.toBinaryString(66706920114753536l));
        //1110110011111101100101110010010110 0000000001 000000000000
 
        String a = "0001100100";//输入数值
        BigInteger src = new BigInteger(a, 2);//转换为BigInteger类型
        System.out.println(src.toString());//转换为2进制并输出结果
 
    }
}

Go语言版本:https://github.com/sumory/idgen
Python语言版本:https://github.com/erans/pysnowflake
打赏作者
微信支付
支付宝
 码字很辛苦,转载请注明来自标点符《高并发环境下生成订单唯一流水号方法:SnowFlake》


在线购物一般是如下流程:
购物车--》生成订单 --》提交订单--》订单确认--》支付订单--》备货--》发货
技术难点在于订单号生成的唯一性,防止订单的重复提交和订单的重复确认。
这儿主要总结后面两点

防止订单重复提交
首先说两个我们购物时经常有过的体验或者说购物网站的网页提醒
  • 你提交的动作过快,请稍后尝试
  • 你的订单已经超时,请刷新页面后重新提交

这些提示说明了该购物网站做了订单提交的限制,一方面是防止有人恶意无限制提交订单,所以限制了一定时间内最大可操作次数,另一方面是为了保证订单无重复提交。
这儿有两个方案:
  • 第一个比较简单,限制某个时间内的最大操作次数只需要有一个计数器就可以(计数器更新需要保证原子性),计数器可以用redis实现,设置一个带有有效时间的值作为计数器,如果值不存在则自动创建(需要保证原子性),超过某一个值就认为操作次数用完即可以实现。
  • 可以使用token机制,token即令牌,提前在订单页面上生成一个token(token生成和使用需要保证原子性)。当提交订单时,根据该token的有效时间和允许的使用次数来判断订单是否允许提交,从而规避重复提交的问题。当然,有人会问,在高并发的情况下,如果是判断token有效之前有很多同一个用户的提交线程过来(用户正常使用一般不会出现这种情况,一般是压力测试工具导致的),那么还是会重复提交,所以,这里需要用到锁机制,访问同一个用户的token同一时间只能有一个线程,token使用之后失效了就会被清楚掉,之后的线程就找不到该token,从而认为订单不能提交(这个加锁是难点)。

防止订单重复确认
如支付宝和微信等,支付宝和微信本身是怎么限制订单只能支付一次的呢?订单怎么保证只会被支付一次呢?这个问题相对来说就简单很多了,同一订单的状态更新的SQL只需要带上条件,利用的是数据库的行锁,推荐!
update table item set item.status=:newstatus where item.id = :id and item.status = oldstatus
当然,如果是分布式系统,这里涉及到的问题会更多

java.lang.Runtime

Runtime类封装了Java运行时的环境。每一个java程序实际上都是启动了一个JVM进程,那么每个JVM进程都是对应这一个Runtime实例,此实例是由JVM为其实例化的。每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。
由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个。所以应该使用单例来实现。
public class Runtime {
private static Runtime currentRuntime = new Runtime();

public static Runtime getRuntime() {
return currentRuntime;
}

private Runtime() {}}
以上代码为JDK中Runtime类的部分实现,可以看到,这其实是饿汉式单例模式。在该类第一次被classloader加载的时候,这个实例就被创建出来了。
一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。

GUI中的单例

除了Runtime是典型的单例以外。JDK中还有几个类是单例的,他们都是GUI中的类。这几个单例的类和Runtime最大的区别就在于他们并不是饿汉模式,也就是他们都是惰性初始化的懒汉单例。如果分析其原因的话也比较简单:那就是他们并不需要事先创建好,只要在第一次真正用到的时候再创建就可以了。因为很多时候我们并不是用Java的GUI和其中的对象。如果使用饿汉单例的话会影响JVM的启动速度。
由于Java的强项并不是做GUI,所以这几个类其实并不会经常被用到。笔者也没用过。把代码贴到这里,从单例的实现的角度简单分析一下。

java.awt.Toolkit#getDefaultToolkit()

public abstract class Toolkit {
/**
* The default toolkit.
*/
private static Toolkit toolkit;

public static synchronized Toolkit getDefaultToolkit() {
if (toolkit == null) {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
Class<?> cls = null;
String nm = System.getProperty("awt.toolkit");
try {
cls = Class.forName(nm);
} catch (ClassNotFoundException e) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
if (cl != null) {
try {
cls = cl.loadClass(nm);
} catch (final ClassNotFoundException ignored) {
throw new AWTError("Toolkit not found: " + nm);
}
}
}
try {
if (cls != null) {
toolkit = (Toolkit)cls.newInstance();
if (GraphicsEnvironment.isHeadless()) {
toolkit = new HeadlessToolkit(toolkit);
}
}
} catch (final InstantiationException ignored) {
throw new AWTError("Could not instantiate Toolkit: " + nm);
} catch (final IllegalAccessException ignored) {
throw new AWTError("Could not access Toolkit: " + nm);
}
return null;
}
});
loadAssistiveTechnologies();
}
return toolkit;
}
}
上面的代码是Toolkit类的单例实现。这里类加载时只静态声明了私有toolkit并没有创建Toolkit实例对象,延迟加载加快了JVM启动速度。
单例模式作为一种创建模式,这里在依赖加载的时候应用了另一种创建对象的方式,不是new新的对象,因为Toolkit本身是个抽象类不能实例化对象,而是通过反射机制加载类并创建新的实例。

java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment()

public abstract class GraphicsEnvironment {
private static GraphicsEnvironment localEnv;
public static synchronized GraphicsEnvironment getLocalGraphicsEnvironment() {
if (localEnv == null) {
localEnv = createGE();
}

return localEnv;
}}
这里类加载时只静态声明了私有localEnv并没有创建实例对象。在GraphicsEnvironment类被第一次调用时会创建该对象。这里没有贴出的createGE()方法也是通过反射的方式创建对象的。

java.awt.Desktop#getDesktop()

public class Desktop {

public static synchronized Desktop getDesktop(){
if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
if (!Desktop.isDesktopSupported()) {
throw new UnsupportedOperationException("Desktop API is not " +
"supported on the current platform");
}

sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
Desktop desktop = (Desktop)context.get(Desktop.class);

if (desktop == null) {
desktop = new Desktop();
context.put(Desktop.class, desktop);
}

return desktop;
}}
上面的代码看上去和单例不太一样。但是实际上也是线程安全的懒汉式单例。获取对象的时候先去环境容器中查找是否存在,不存在实例则创建一个实例。
以上三个类的获取实例的方法都通过同步方法的方式保证了线程安全。
Runtime类是通过静态初始化的方式保证其线程安全的。

总结

文中介绍了四个单例的例子,其中有一个是饿汉式单例,三个是懒汉式单例。通过JDK中的实际应用我们可以得出以下结论:
当一个类的对象只需要或者只可能有一个时,应该考虑单例模式。
如果一个类的实例应该在JVM初始化时被创建出来,应该考虑使用饿汉式单例。
如果一个类的实例不需要预先被创建,也许这个类的实例并不一定能用得上,也许这个类的实例创建过程比较耗费时间,也许就是真的没必须提前创建。那么应该考虑懒汉式单例。
在使用懒汉式单例的时候,应该考虑到线程的安全性问题。


#0 系列目录#
#1 秒杀业务分析#
  1. 正常电子商务流程
(1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货
  1. 秒杀业务的特性
(1)低廉价格;(2)大幅推广;(3)瞬时售空;(4)一般是定时上架;(5)时间短、瞬时并发量高;
#2 秒杀技术挑战# 假设某网站秒杀活动只推出一件商品,预计会吸引1万人参加活动,也就说最大并发请求数是10000,秒杀系统需要面对的技术挑战有:
  1. 对现有网站业务造成冲击
秒杀活动只是网站营销的一个附加活动,这个活动具有时间短,并发访问量大的特点,如果和网站原有应用部署在一起,必然会对现有业务造成冲击,稍有不慎可能导致整个网站瘫痪。
解决方案:将秒杀系统独立部署,甚至使用独立域名,使其与网站完全隔离
  1. 高并发下的应用、数据库负载
用户在秒杀开始前,通过不停刷新浏览器页面以保证不会错过秒杀,这些请求如果按照一般的网站应用架构,访问应用服务器、连接数据库,会对应用服务器和数据库服务器造成负载压力。
解决方案:重新设计秒杀商品页面,不使用网站原来的商品详细页面,页面内容静态化,用户请求不需要经过应用服务
  1. 突然增加的网络及服务器带宽
假设商品页面大小200K(主要是商品图片大小),那么需要的网络和服务器带宽是2G(200K×10000),这些网络带宽是因为秒杀活动新增的,超过网站平时使用的带宽。
解决方案:因为秒杀新增的网络带宽,必须和运营商重新购买或者租借。为了减轻网站服务器的压力,需要将秒杀商品页面缓存在CDN,同样需要和CDN服务商临时租借新增的出口带宽
  1. 直接下单
秒杀的游戏规则是到了秒杀才能开始对商品下单购买,在此时间点之前,只能浏览商品信息,不能下单。而下单页面也是一个普通的URL,如果得到这个URL,不用等到秒杀开始就可以下单了。
解决方案:为了避免用户直接访问下单页面URL,需要将改URL动态化,即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。办法是在下单页面URL加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到
  1. 如何控制秒杀商品页面购买按钮的点亮
购买按钮只有在秒杀开始的时候才能点亮,在此之前是灰色的。如果该页面是动态生成的,当然可以在服务器端构造响应页面输出,控制该按钮是灰色还 是点亮,但是为了减轻服务器端负载压力,更好地利用CDN、反向代理等性能优化手段,该页面被设计为静态页面,缓存在CDN、反向代理服务器上,甚至用户浏览器上。秒杀开始时,用户刷新页面,请求根本不会到达应用服务器。
解决方案:使用JavaScript脚本控制,在秒杀商品静态页面中加入一个JavaScript文件引用,该JavaScript文件中包含 秒杀开始标志为否;当秒杀开始的时候生成一个新的JavaScript文件(文件名保持不变,只是内容不一样),更新秒杀开始标志为是,加入下单页面的URL及随机数参数(这个随机数只会产生一个,即所有人看到的URL都是同一个,服务器端可以用redis这种分布式缓存服务器来保存随机数),并被用户浏览器加载,控制秒杀商品页面的展示。这个JavaScript文件的加载可以加上随机版本号(例如xx.js?v=32353823),这样就不会被浏览器、CDN和反向代理服务器缓存
这个JavaScript文件非常小,即使每次浏览器刷新都访问JavaScript文件服务器也不会对服务器集群和网络带宽造成太大压力。
  1. 如何只允许第一个提交的订单被发送到订单子系统
由于最终能够成功秒杀到商品的用户只有一个,因此需要在用户提交订单时,检查是否已经有订单提交。如果已经有订单提交成功,则需要更新 JavaScript文件,更新秒杀开始标志为否,购买按钮变灰。事实上,由于最终能够成功提交订单的用户只有一个,为了减轻下单页面服务器的负载压力, 可以控制进入下单页面的入口,只有少数用户能进入下单页面,其他用户直接进入秒杀结束页面。
解决方案:假设下单服务器集群有10台服务器,每台服务器只接受最多10个下单请求。在还没有人提交订单成功之前,如果一台服务器已经有十单了,而有的一单都没处理,可能出现的用户体验不佳的场景是用户第一次点击购买按钮进入已结束页面,再刷新一下页面,有可能被一单都没有处理的服务器处理,进入了填写订单的页面,可以考虑通过cookie的方式来应对,符合一致性原则。当然可以采用最少连接的负载均衡算法,出现上述情况的概率大大降低。
  1. 如何进行下单前置检查
  • 下单服务器检查本机已处理的下单请求数目:
如果超过10条,直接返回已结束页面给用户;
如果未超过10条,则用户可进入填写订单及确认页面;
  • 检查全局已提交订单数目:
已超过秒杀商品总数,返回已结束页面给用户;
未超过秒杀商品总数,提交到子订单系统;
  1. 秒杀一般是定时上架
该功能实现方式很多。不过目前比较好的方式是:提前设定好商品的上架时间,用户可以在前台看到该商品,但是无法点击“立即购买”的按钮。但是需要考虑的是,有人可以绕过前端的限制,直接通过URL的方式发起购买,这就需要在前台商品页面,以及bug页面到后端的数据库,都要进行时钟同步。越在后端控制,安全性越高。
定时秒杀的话,就要避免卖家在秒杀前对商品做编辑带来的不可预期的影响。这种特殊的变更需要多方面评估。一般禁止编辑,如需变更,可以走数据订正多的流程。
  1. 减库存的操作
有两种选择,一种是拍下减库存 另外一种是付款减库存;目前采用的“拍下减库存”的方式,拍下就是一瞬间的事,对用户体验会好些。
  1. 库存会带来“超卖”的问题:售出数量多于库存数量
由于库存并发更新的问题,导致在实际库存已经不足的情况下,库存依然在减,导致卖家的商品卖得件数超过秒杀的预期。方案:采用乐观锁
update auction_auctions set
quantity = #inQuantity#where auction_id = #itemId# and quantity = #dbQuantity#
还有一种方式,会更好些,叫做尝试扣减库存,扣减库存成功才会进行下单逻辑:
update auction_auctions set
quantity = quantity-#count#
where auction_id = #itemId# and quantity >= #count#
  1. 秒杀器的应对
秒杀器一般下单个购买及其迅速,根据购买记录可以甄别出一部分。可以通过校验码达到一定的方法,这就要求校验码足够安全,不被破解,采用的方式有:秒杀专用验证码,电视公布验证码,秒杀答题
#3 秒杀架构原则#
  1. 尽量将请求拦截在系统上游
传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小【一趟火车其实只有2000张票,200w个人来买,基本没有人能买成功,请求有效率为0】。
  1. 读多写少的常用多使用缓存
这是一个典型的读多写少的应用场景【一趟火车其实只有2000张票,200w个人来买,最多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%】,非常适合使用缓存
#4 秒杀架构设计# 秒杀系统为秒杀而设计,不同于一般的网购行为,参与秒杀活动的用户更关心的是如何能快速刷新商品页面,在秒杀开始的时候抢先进入下单页面,而不是商品详情等用户体验细节,因此秒杀系统的页面设计应尽可能简单。
商品页面中的购买按钮只有在秒杀活动开始的时候才变亮,在此之前及秒杀商品卖出后,该按钮都是灰色的,不可以点击。
下单表单也尽可能简单,购买数量只能是一个且不可以修改,送货地址和付款方式都使用用户默认设置,没有默认也可以不填,允许等订单提交后修改;只有第一个提交的订单发送给网站的订单子系统,其余用户提交订单后只能看到秒杀结束页面。
要做一个这样的秒杀系统,业务会分为两个阶段,第一个阶段是秒杀开始前某个时间到秒杀开始, 这个阶段可以称之为准备阶段,用户在准备阶段等待秒杀; 第二个阶段就是秒杀开始到所有参与秒杀的用户获得秒杀结果, 这个就称为秒杀阶段吧。
##4.1 前端层设计## 首先要有一个展示秒杀商品的页面, 在这个页面上做一个秒杀活动开始的倒计时, 在准备阶段内用户会陆续打开这个秒杀的页面, 并且可能不停的刷新页面。这里需要考虑两个问题:
  1. 第一个是秒杀页面的展示
我们知道一个html页面还是比较大的,即使做了压缩,http头和内容的大小也可能高达数十K,加上其他的css, js,图片等资源,如果同时有几千万人参与一个商品的抢购,一般机房带宽也就只有1G~10G,网络带宽就极有可能成为瓶颈,所以这个页面上各类静态资源首先应分开存放,然后放到cdn节点上分散压力,由于CDN节点遍布全国各地,能缓冲掉绝大部分的压力,而且还比机房带宽便宜~
  1. 第二个是倒计时
出于性能原因这个一般由js调用客户端本地时间,就有可能出现客户端时钟与服务器时钟不一致,另外服务器之间也是有可能出现时钟不一致。客户端与服务器时钟不一致可以采用客户端定时和服务器同步时间,这里考虑一下性能问题,用于同步时间的接口由于不涉及到后端逻辑,只需要将当前web服务器的时间发送给客户端就可以了,因此速度很快,就我以前测试的结果来看,一台标准的web服务器2W+QPS不会有问题,如果100W人同时刷,100W QPS也只需要50台web,一台硬件LB就可以了~,并且web服务器群是可以很容易的横向扩展的(LB+DNS轮询),这个接口可以只返回一小段json格式的数据,而且可以优化一下减少不必要cookie和其他http头的信息,所以数据量不会很大,一般来说网络不会成为瓶颈,即使成为瓶颈也可以考虑多机房专线连通,加智能DNS的解决方案;web服务器之间时间不同步可以采用统一时间服务器的方式,比如每隔1分钟所有参与秒杀活动的web服务器就与时间服务器做一次时间同步
  1. 浏览器层请求拦截
(1)产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求;
(2)JS层面,限制用户在x秒之内只能提交一次请求;
##4.2 站点层设计## 前端层的请求拦截,只能拦住小白用户(不过这是99%的用户哟),高端的程序员根本不吃这一套,写个for循环,直接调用你后端的http请求,怎么整?
(1)同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面
(2)同一个item的查询,例如手机车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面
如此限流,又有99%的流量会被拦截在站点层。 ##4.3 服务层设计## 站点层的请求拦截,只能拦住普通程序员,高级黑客,假设他控制了10w台肉鸡(并且假设买票不需要实名认证),这下uid的限制不行了吧?怎么整?
(1)大哥,我是服务层,我清楚的知道小米只有1万部手机,我清楚的知道一列火车只有2000张车票,我透10w个请求去数据库有什么意义呢?对于写请求,做请求队列,每次只透过有限的写请求去数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”
(2)对于读请求,还用说么?cache来抗,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的;
如此限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去,又有99.9%的请求被拦住了。
  1. 用户请求分发模块:使用Nginx或Apache将用户的请求分发到不同的机器上。
  2. 用户请求预处理模块:判断商品是不是还有剩余来决定是不是要处理该请求。
  3. 用户请求处理模块:把通过预处理的请求封装成事务提交给数据库,并返回是否成功。
  4. 数据库接口模块:该模块是数据库的唯一接口,负责与数据库交互,提供RPC接口供查询是否秒杀结束、剩余数量等信息。
  • 用户请求预处理模块
经过HTTP服务器的分发后,单个服务器的负载相对低了一些,但总量依然可能很大,如果后台商品已经被秒杀完毕,那么直接给后来的请求返回秒杀失败即可,不必再进一步发送事务了,示例代码可以如下所示:
package seckill;
import org.apache.http.HttpRequest;
/**
* 预处理阶段,把不必要的请求直接驳回,必要的请求添加到队列中进入下一阶段.
*/public class PreProcessor {
// 商品是否还有剩余
private static boolean reminds = true;
private static void forbidden() {
// Do something.
}
public static boolean checkReminds() {
if (reminds) {
// 远程检测是否还有剩余,该RPC接口应由数据库服务器提供,不必完全严格检查.
if (!RPC.checkReminds()) {
reminds = false;
}
}
return reminds;
}
/**
* 每一个HTTP请求都要经过该预处理.
*/
public static void preProcess(HttpRequest request) {
if (checkReminds()) {
// 一个并发的队列
RequestQueue.queue.add(request);
} else {
// 如果已经没有商品了,则直接驳回请求即可.
forbidden();
}
}
}
  • 并发队列的选择
Java的并发包提供了三个常用的并发队列实现,分别是:ConcurrentLinkedQueue 、 LinkedBlockingQueue 和 ArrayBlockingQueue。
ArrayBlockingQueue是初始容量固定的阻塞队列,我们可以用来作为数据库模块成功竞拍的队列,比如有10个商品,那么我们就设定一个10大小的数组队列。
ConcurrentLinkedQueue使用的是CAS原语无锁队列实现,是一个异步队列,入队的速度很快,出队进行了加锁,性能稍慢。
LinkedBlockingQueue也是阻塞的队列,入队和出队都用了加锁,当队空的时候线程会暂时阻塞。
由于我们的系统入队需求要远大于出队需求,一般不会出现队空的情况,所以我们可以选择ConcurrentLinkedQueue来作为我们的请求队列实现:
package seckill;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.http.HttpRequest;
public class RequestQueue {
public static ConcurrentLinkedQueue<HttpRequest> queue = new ConcurrentLinkedQueue<HttpRequest>();
}
  • 用户请求模块
package seckill;
import org.apache.http.HttpRequest;
public class Processor {
/**
* 发送秒杀事务到数据库队列.
*/
public static void kill(BidInfo info) {
DB.bids.add(info);
}
public static void process() {
BidInfo info = new BidInfo(RequestQueue.queue.poll());
if (info != null) {
kill(info);
}
}
}
class BidInfo {
BidInfo(HttpRequest request) {
// Do something.
}
}
  • 数据库模块
数据库主要是使用一个ArrayBlockingQueue来暂存有可能成功的用户请求。
package seckill;
import java.util.concurrent.ArrayBlockingQueue;
/**
* DB应该是数据库的唯一接口.
*/public class DB {
public static int count = 10;
public static ArrayBlockingQueue<BidInfo> bids = new ArrayBlockingQueue<BidInfo>(10);
public static boolean checkReminds() {
// TODO
return true;
}
// 单线程操作
public static void bid() {
BidInfo info = bids.poll();
while (count-- > 0) {
// insert into table Bids values(item_id, user_id, bid_date, other)
// select count(id) from Bids where item_id = ?
// 如果数据库商品数量大约总数,则标志秒杀已完成,设置标志位reminds = false.
info = bids.poll();
}
}
}
##4.4 数据库设计## ###4.4.1 基本概念### 概念一“单库”
概念二“分片”
分片解决的是“数据量太大”的问题,也就是通常说的“水平切分”。一旦引入分片,势必有“数据路由”的概念,哪个数据访问哪个库。路由规则通常有3种方法:
  1. 范围:range
优点:简单,容易扩展
缺点:各库压力不均(新号段更活跃)
  1. 哈希:hash 【大部分互联网公司采用的方案二:哈希分库,哈希路由】
优点:简单,数据均衡,负载均匀
缺点:迁移麻烦(2库扩3库数据要迁移)
  1. 路由服务:router-config-server
优点:灵活性强,业务与路由算法解耦
缺点:每次访问数据库前多一次查询
概念三“分组”
分组解决“可用性”问题,分组通常通过主从复制的方式实现。
互联网公司数据库实际软件架构是:又分片,又分组(如下图)
###4.4.2 设计思路### 数据库软件架构师平时设计些什么东西呢?至少要考虑以下四点:
  1. 如何保证数据可用性;
  2. 如何提高数据库读性能(大部分应用读多写少,读会先成为瓶颈);
  3. 如何保证一致性;
  4. 如何提高扩展性;
  • 1. 如何保证数据的可用性?
解决可用性问题的思路是=>冗余
如何保证站点的可用性?复制站点,冗余站点
如何保证服务的可用性?复制服务,冗余服务
如何保证数据的可用性?复制数据,冗余数据
数据的冗余,会带来一个副作用=>引发一致性问题(先不说一致性问题,先说可用性)
  • 2. 如何保证数据库“读”高可用?
冗余读库
冗余读库带来的副作用?读写有延时,可能不一致
上面这个图是很多互联网公司mysql的架构,写仍然是单点,不能保证写高可用。
  • 3. 如何保证数据库“写”高可用?
冗余写库
采用双主互备的方式,可以冗余写库带来的副作用?双写同步,数据可能冲突(例如“自增id”同步冲突),如何解决同步冲突,有两种常见解决方案:
  1. 两个写库使用不同的初始值,相同的步长来增加id:1写库的id为0,2,4,6...;2写库的id为1,3,5,7...;
  2. 不使用数据的id,业务层自己生成唯一的id,保证数据不冲突;
实际中没有使用上述两种架构来做读写的“高可用”,采用的是“双主当主从用”的方式
仍是双主,但只有一个主提供服务(读+写),另一个主是“shadow-master”,只用来保证高可用,平时不提供服务。 master挂了,shadow-master顶上(vip漂移,对业务层透明,不需要人工介入)。这种方式的好处:
  1. 读写没有延时;
  2. 读写高可用;
不足:
  1. 不能通过加从库的方式扩展读性能;
  2. 资源利用率为50%,一台冗余主没有提供服务;
那如何提高读性能呢?进入第二个话题,如何提供读性能。
  • 4. 如何扩展读性能
提高读性能的方式大致有三种,第一种是建立索引。这种方式不展开,要提到的一点是,不同的库可以建立不同的索引
写库不建立索引;
线上读库建立线上访问索引,例如uid;
线下读库建立线下访问索引,例如time;
第二种扩充读性能的方式是,增加从库,这种方法大家用的比较多,但是,存在两个缺点:
  1. 从库越多,同步越慢;
  2. 同步越慢,数据不一致窗口越大(不一致后面说,还是先说读性能的提高);
实际中没有采用这种方法提高数据库读性能(没有从库),采用的是增加缓存。常见的缓存架构如下:
上游是业务应用,下游是主库,从库(读写分离),缓存
实际的玩法:服务+数据库+缓存一套
业务层不直接面向db和cache,服务层屏蔽了底层db、cache的复杂性。为什么要引入服务层,今天不展开,采用了“服务+数据库+缓存一套”的方式提供数据访问,用cache提高读性能
不管采用主从的方式扩展读性能,还是缓存的方式扩展读性能,数据都要复制多份(主+从,db+cache),一定会引发一致性问题
  • 5. 如何保证一致性?
主从数据库的一致性,通常有两种解决方案:
1. 中间件
如果某一个key有写操作,在不一致时间窗口内,中间件会将这个key的读操作也路由到主库上。这个方案的缺点是,数据库中间件的门槛较高(百度,腾讯,阿里,360等一些公司有)。
2. 强制读主
上面实际用的“双主当主从用”的架构,不存在主从不一致的问题
第二类不一致,是db与缓存间的不一致
常见的缓存架构如上,此时写操作的顺序是:
(1)淘汰cache;
(2)写数据库;
读操作的顺序是:
(1)读cache,如果cache hit则返回;
(2)如果cache miss,则读从库;
(3)读从库后,将数据放回cache;
在一些异常时序情况下,有可能从【从库读到旧数据(同步还没有完成),旧数据入cache后】,数据会长期不一致。解决办法是“缓存双淘汰”,写操作时序升级为:
(1)淘汰cache;
(2)写数据库;
(3)在经验“主从同步延时窗口时间”后,再次发起一个异步淘汰cache的请求;
这样,即使有脏数据如cache,一个小的时间窗口之后,脏数据还是会被淘汰。带来的代价是,多引入一次读miss(成本可以忽略)。
除此之外,最佳实践之一是:建议为所有cache中的item设置一个超时时间
  • 6. 如何提高数据库的扩展性?
原来用hash的方式路由,分为2个库,数据量还是太大,要分为3个库,势必需要进行数据迁移,有一个很帅气的“数据库秒级扩容”方案。
如何秒级扩容?
首先,我们不做2库变3库的扩容,我们做2库变4库(库加倍)的扩容(未来4->8->16)
服务+数据库是一套(省去了缓存),数据库采用“双主”的模式
扩容步骤:
第一步,将一个主库提升;
第二步,修改配置,2库变4库(原来MOD2,现在配置修改后MOD4),扩容完成;
原MOD2为偶的部分,现在会MOD4余0或者2;原MOD2为奇的部分,现在会MOD4余1或者3;数据不需要迁移,同时,双主互相同步,一遍是余0,一边余2,两边数据同步也不会冲突,秒级完成扩容!
最后,要做一些收尾工作:
  1. 将旧的双主同步解除;
  2. 增加新的双主(双主是保证可用性的,shadow-master平时不提供服务);
  3. 删除多余的数据(余0的主,可以将余2的数据删除掉);
这样,秒级别内,我们就完成了2库变4库的扩展。
#5 大并发带来的挑战# ##5.1 请求接口的合理设计## 一个秒杀或者抢购页面,通常分为2个部分,一个是静态的HTML等内容,另一个就是参与秒杀的Web后台请求接口
通常静态HTML等内容,是通过CDN的部署,一般压力不大,核心瓶颈实际上在后台请求接口上。这个后端接口,必须能够支持高并发请求,同时,非常重要的一点,必须尽可能“快”,在最短的时间里返回用户的请求结果。为了实现尽可能快这一点,接口的后端存储使用内存级别的操作会更好一点。仍然直接面向MySQL之类的存储是不合适的,如果有这种复杂业务的需求,都建议采用异步写入
当然,也有一些秒杀和抢购采用“滞后反馈”,就是说秒杀当下不知道结果,一段时间后才可以从页面中看到用户是否秒杀成功。但是,这种属于“偷懒”行为,同时给用户的体验也不好,容易被用户认为是“暗箱操作”。
##5.2 高并发的挑战:一定要“快”## 我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标非常关键。举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大连接数目)。
那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):
20*500/0.1 = 100000 (10万QPS)
咦?我们的系统似乎很强大,1秒钟可以处理完10万的请求,5w/s的秒杀似乎是“纸老虎”哈。实际情况,当然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增加
就Web服务器而言,Apache打开了越多的连接进程,CPU需要处理的上下文切换也越多,额外增加了CPU的消耗,然后就直接导致平均响应时间增加。因此上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。可以通过Apache自带的abench来测试一下,取一个合适的值。然后,我们选择内存操作级别的存储的Redis,在高并发的状态下,存储的响应时间至关重要。网络带宽虽然也是一个因素,不过,这种请求数据包一般比较小,一般很少成为请求的瓶颈。负载均衡成为系统瓶颈的情况比较少,在这里不做讨论哈。
那么问题来了,假设我们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际情况,甚至更多):
20*500/0.25 = 40000 (4万QPS)
于是,我们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。
然后,这才是真正的恶梦开始。举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定出现大塞车。(5条车道忽然变成4条车道的感觉)。
同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。
其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数占满,其他正常的业务请求,无连接进程可用。
更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。
##5.3 重启与过载保护## 如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间
秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回
#6 作弊的手段:进攻与防守# 秒杀和抢购收到了“海量”的请求,实际上里面的水分是很大的。不少用户,为了“抢“到商品,会使用“刷票工具”等类型的辅助工具,帮助他们发送尽可能多的请求到服务器。还有一部分高级用户,制作强大的自动请求脚本。这种做法的理由也很简单,就是在参与秒杀和抢购的请求中,自己的请求数目占比越多,成功的概率越高
这些都是属于“作弊的手段”,不过,有“进攻”就有“防守”,这是一场没有硝烟的战斗哈。
##6.1 同一个账号,一次性发出多个请求## 部分用户通过浏览器的插件或者其他工具,在秒杀开始的时间里,以自己的账号,一次发送上百甚至更多的请求。实际上,这样的用户破坏了秒杀和抢购的公平性。
这种请求在某些没有做数据安全处理的系统里,也可能造成另外一种破坏,导致某些判断条件被绕过。例如一个简单的领取逻辑,先判断用户是否有参与记录,如果没有则领取成功,最后写入到参与记录中。这是个非常简单的逻辑,但是,在高并发的场景下,存在深深的漏洞。多个并发请求通过负载均衡服务器,分配到内网的多台Web服务器,它们首先向存储发送查询请求,然后,在某个请求成功写入参与记录的时间差内,其他的请求获查询到的结果都是“没有参与记录”。这里,就存在逻辑判断被绕过的风险。
应对方案:
在程序入口处,一个账号只允许接受1个请求,其他请求过滤。不仅解决了同一个账号,发送N个请求的问题,还保证了后续的逻辑流程的安全。实现方案,可以通过Redis这种内存缓存服务,写入一个标志位(只允许1个请求写成功,结合watch的乐观锁的特性),成功写入的则可以继续参加
或者,自己实现一个服务,将同一个账号的请求放入一个队列中,处理完一个,再处理下一个。
##6.2 多个账号,一次性发送多个请求## 很多公司的账号注册功能,在发展早期几乎是没有限制的,很容易就可以注册很多个账号。因此,也导致了出现了一些特殊的工作室,通过编写自动注册脚本,积累了一大批“僵尸账号”,数量庞大,几万甚至几十万的账号不等,专门做各种刷的行为(这就是微博中的“僵尸粉“的来源)。举个例子,例如微博中有转发抽奖的活动,如果我们使用几万个“僵尸号”去混进去转发,这样就可以大大提升我们中奖的概率。
这种账号,使用在秒杀和抢购里,也是同一个道理。例如,iPhone官网的抢购,火车票黄牛党。
应对方案:
这种场景,可以通过检测指定机器IP请求频率就可以解决,如果发现某个IP请求频率很高,可以给它弹出一个验证码或者直接禁止它的请求
  1. 弹出验证码,最核心的追求,就是分辨出真实用户。因此,大家可能经常发现,网站弹出的验证码,有些是“鬼神乱舞”的样子,有时让我们根本无法看清。他们这样做的原因,其实也是为了让验证码的图片不被轻易识别,因为强大的“自动脚本”可以通过图片识别里面的字符,然后让脚本自动填写验证码。实际上,有一些非常创新的验证码,效果会比较好,例如给你一个简单问题让你回答,或者让你完成某些简单操作(例如百度贴吧的验证码)。
  2. 直接禁止IP,实际上是有些粗暴的,因为有些真实用户的网络场景恰好是同一出口IP的,可能会有“误伤“。但是这一个做法简单高效,根据实际场景使用可以获得很好的效果。
##6.3 多个账号,不同IP发送不同请求## 所谓道高一尺,魔高一丈。有进攻,就会有防守,永不休止。这些“工作室”,发现你对单机IP请求频率有控制之后,他们也针对这种场景,想出了他们的“新进攻方案”,就是不断改变IP
有同学会好奇,这些随机IP服务怎么来的。有一些是某些机构自己占据一批独立IP,然后做成一个随机代理IP的服务,有偿提供给这些“工作室”使用。还有一些更为黑暗一点的,就是通过木马黑掉普通用户的电脑,这个木马也不破坏用户电脑的正常运作,只做一件事情,就是转发IP包,普通用户的电脑被变成了IP代理出口。通过这种做法,黑客就拿到了大量的独立IP,然后搭建为随机IP服务,就是为了挣钱。
应对方案:
说实话,这种场景下的请求,和真实用户的行为,已经基本相同了,想做分辨很困难。再做进一步的限制很容易“误伤“真实用户,这个时候,通常只能通过设置业务门槛高来限制这种请求了,或者通过账号行为的”数据挖掘“来提前清理掉它们
僵尸账号也还是有一些共同特征的,例如账号很可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特点,适当设置参与门槛,例如限制参与秒杀的账号等级。通过这些业务手段,也是可以过滤掉一些僵尸号
#7 高并发下的数据安全# 我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。
##7.1 超发的原因## 假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。
在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。
##7.2 悲观锁思路## 解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。
悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。
虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常
##7.3 FIFO队列思路## 那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。
然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。
##7.4 乐观锁思路## 这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。
有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。
#8 总结# 互联网正在高速发展,使用互联网服务的用户越多,高并发的场景也变得越来越多。电商秒杀和抢购,是两个比较典型的互联网高并发场景。虽然我们解决问题的具体技术方案可能千差万别,但是遇到的挑战却是相似的,因此解决问题的思路也异曲同工
原文地址:https://my.oschina.net/xianggao/blog/524943

核心要点:
我们读取数据一般是按照字节读取的,因为java当中的字节是有符号型,所以如果我们想把它转换为无符号型需要先隐式转换为int,再进行0XFF的&运算,然后将结果用比byte大的数据类型接收(一般是int)。
理解难点在于负数byte到int的转换,二进制位会发生变化,高位会全部补1,因此需要一个0XFF的&运算将高位的1抹掉
在C语言中
char,类型识别符,字符型。 
[signed]char 有符号字符型 长度(字节)为 -128~127 
unsigned char 无符号字符型 长度(字节)为 0~255
在Java中
char 16位 范围是2负的2的15次方到2的15次方的整数
byte 虽然是8位,但是取值范围是负的2的7次方到2的7次方的整数
在Java中,不存在Unsigned无符号数据类型,但可以轻而易举的完成Unsigned转换。 
方案一:如果在Java中进行流(Stream)数据处理,可以用DataInputStream类对Stream中的数据以Unsigned读取。 
Java在这方面提供了支持,可以用java.io.DataInputStream 类对象来完成对流内数据的Unsigned读取,该类提供了如下方法: 
(1)int readUnsignedByte () //从流中读取一个0~255(0xFF)的单字节数据,并以int数据类型的数据返回。返回的数据相当于C/C++语言中所谓的“BYTE”。 
(2)int readUnsignedShort () //从流中读取一个0~65535(0xFFFF)的双字节数据,并以int数据类型的数据返回。返回的数据相当于C/C++语言中所谓的“WORD”, 并且是以“低地址低字节”的方式返回的,所以程序员不需要额外的转换。 
方案二:利用Java位运算符,完成Unsigned转换。 
正常情况下,Java提供的数据类型是有符号signed类型的,可以通过位运算的方式得到它们相对应的无符号值,参见几个方法中的代码: 
public int getUnsignedByte (byte data){ //将data字节型数据转换为0~255 (0xFF 即BYTE)。 
return data&0x0FF ; 
public int getUnsignedByte (short data){ //将data字节型数据转换为0~65535 (0xFFFF 即 WORD)。 
return data&0x0FFFF ; 
public long getUnsignedIntt (int data){ //将int数据转换为0~4294967295 (0xFFFFFFFF即DWORD)。 
return data&0x0FFFFFFFF ; 
}


前言:

身为一个java程序员,怎么能不了解JVM呢,倘若想学习JVM,那就又必须要了解Class文件,Class之于虚拟机,就如鱼之于水,虚拟机因为Class而有了生命。《深入理解java虚拟机》中花了一整个章节来讲解Class文件,可是看完后,一直都还是迷迷糊糊,似懂非懂。正好前段时间看见一本书很不错:《自己动手写Java虚拟机》,作者利用go语言实现了一个简单的JVM,虽然没有完整实现JVM的所有功能,但是对于一些对JVM稍感兴趣的人来说,可读性还是很高的。作者讲解的很详细,每个过程都分为了一章,其中一部分就是讲解如何解析Class文件。
这本书不太厚,很快就读完了,读完后,收获颇丰。但是纸上得来终觉浅,绝知此事要躬行,我便尝试着自己解析Class文件。go语言虽然很优秀,但是终究不熟练,尤其是不太习惯其把类型放在变量之后的语法,还是老老实实用java吧。
话不多说,先贴出项目地址:https://github.com/HalfStackDeveloper/ClassReader

Class文件

什么是Class文件?

java之所以能够实现跨平台,便在于其编译阶段不是将代码直接编译为平台相关的机器语言,而是先编译成二进制形式的java字节码,放在Class文件之中,虚拟机再加载Class文件,解析出程序运行所需的内容。每个类都会被编译成一个单独的class文件,内部类也会作为一个独立的类,生成自己的class。

基本结构

随便找到一个class文件,用Sublime Text打开是这样的:
是不是一脸懵逼,不过java虚拟机规范中给出了class文件的基本格式,只要按照这个格式去解析就可以了:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
ClassFile中的字段类型有u1、u2、u4,这是什么类型呢?其实很简单,就是分别表示1个字节,2个字节和4个字节。
开头四个字节为:magic,是用来唯一标识文件格式的,一般被称作magic number(魔数),这样虚拟机才能识别出所加载的文件是否是class格式,class文件的魔数为cafebabe。不只是class文件,基本上大部分文件都有魔数,用来标识自己的格式。
接下来的部分主要是class文件的一些信息,如常量池、类访问标志、父类、接口信息、字段、方法等,具体的信息可参考《Java虚拟机规范》。

解析

字段类型

上面说到ClassFile中的字段类型有u1、u2、u4,分别表示1个字节,2个字节和4个字节的无符号整数。java中short、int、long分别为2、4、8个字节的有符号整数,去掉符号位,刚好可以用来表示u1、u2、u4。
public class U1 {
public static short read(InputStream inputStream) {
byte[] bytes = new byte[1];
try {
inputStream.read(bytes);
} catch (IOException e) {
e.printStackTrace();
}
short value = (short) (bytes[0] & 0xFF);
return value;
}
}

public class U2 {
public static int read(InputStream inputStream) {
byte[] bytes = new byte[2];
try {
inputStream.read(bytes);
} catch (IOException e) {
e.printStackTrace();
}
int num = 0;
for (int i= 0; i < bytes.length; i++) {
num <<= 8;
num |= (bytes[i] & 0xff);
}
return num;
}
}

public class U4 {
public static long read(InputStream inputStream) {
byte[] bytes = new byte[4];
try {
inputStream.read(bytes);
} catch (IOException e) {
e.printStackTrace();
}
long num = 0;
for (int i= 0; i < bytes.length; i++) {
num <<= 8;
num |= (bytes[i] & 0xff);
}
return num;
}
}

常量池

定义好字段类型后,我们就可以读取class文件了,首先是读取魔数之类的基本信息,这部分很简单:
FileInputStream inputStream = new FileInputStream(file);
ClassFile classFile = new ClassFile();
classFile.magic = U4.read(inputStream);
classFile.minorVersion = U2.read(inputStream);
classFile.majorVersion = U2.read(inputStream);
这部分只是热热身,接下来的大头在于常量池。解析常量池之前,我们先来解释一下常量池是什么。
常量池,顾名思义,存放常量的资源池,这里的常量指的是字面量和符号引用。字面量指的是一些字符串资源,而符号引用分为三类:类符号引用、方法符号引用和字段符号引用。通过将资源放在常量池中,其他项就可以直接定义成常量池中的索引了,避免了空间的浪费,不只是class文件,Android可执行文件dex也是同样如此,将字符串资源等放在DexData中,其他项通过索引定位资源。java虚拟机规范给出了常量池中每一项的格式:
cp_info {
u1 tag;
u1 info[];
}
上面的这个格式只是一个通用格式,常量池中真正包含的数据有14种格式,每种格式的tag值不同,具体如下所示:
由于格式太多,文章中只挑选一部分讲解:
这里首先读取常量池的大小,初始化常量池:
//解析常量池
int constant_pool_count = U2.read(inputStream);
ConstantPool constantPool = new ConstantPool(constant_pool_count);
constantPool.read(inputStream);
接下来再逐个读取每项内容,并存储到数组cpInfo中,这里需要注意的是,cpInfo[]下标从1开始,0无效,且真正的常量池大小为constant_pool_count-1。
public class ConstantPool {
public int constant_pool_count;
public ConstantInfo[] cpInfo;

public ConstantPool(int count) {
constant_pool_count = count;
cpInfo = new ConstantInfo[constant_pool_count];
}

public void read(InputStream inputStream) {
for (int i = 1; i < constant_pool_count; i++) {
short tag = U1.read(inputStream);
ConstantInfo constantInfo = ConstantInfo.getConstantInfo(tag);
constantInfo.read(inputStream);
cpInfo[i] = constantInfo;
if (tag == ConstantInfo.CONSTANT_Double || tag == ConstantInfo.CONSTANT_Long) {
i++;
}
}
}
}
我们先来看看CONSTANT_Utf8格式,这一项里面存放的是MUTF-8编码的字符串:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
那么如何读取这一项呢?
public class ConstantUtf8 extends ConstantInfo {
public String value;

@Override
public void read(InputStream inputStream) {
int length = U2.read(inputStream);
byte[] bytes = new byte[length];
try {
inputStream.read(bytes);
} catch (IOException e) {
e.printStackTrace();
}
try {
value = readUtf8(bytes);
} catch (UTFDataFormatException e) {
e.printStackTrace();
}
}

private String readUtf8(byte[] bytearr) throws UTFDataFormatException {
//copy from java.io.DataInputStream.readUTF()
}
}
很简单,首先读取这一项的字节数组长度,接着调用readUtf8(),将字节数组转化为String字符串。
再来看看CONSTANT_Class这一项,这一项存储的是类或者接口的符号引用:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
注意这里的name_index并不是直接的字符串,而是指向常量池中cpInfo数组的name_index项,且cpInfo[name_index]一定是CONSTANT_Utf8格式。
public class ConstantClass extends ConstantInfo {
public int nameIndex;

@Override
public void read(InputStream inputStream) {
nameIndex = U2.read(inputStream);
}
}
常量池解析完毕后,就可以供后面的数据使用了,比方说ClassFile中的this_class指向的就是常量池中格式为CONSTANT_Class的某一项,那么我们就可以读取出类名:
int classIndex = U2.read(inputStream);
ConstantClass clazz = (ConstantClass) constantPool.cpInfo[classIndex];
ConstantUtf8 className = (ConstantUtf8) constantPool.cpInfo[clazz.nameIndex];
classFile.className = className.value;
System.out.print("classname:" + classFile.className + "\n");

字节码指令

解析常量池之后还需要接着解析一些类信息,如父类、接口类、字段等,但是相信大家最好奇的还是java指令的存储,大家都知道,我们平时写的java代码会被编译成java字节码,那么这些字节码到底存储在哪呢?别急,讲解指令之前,我们先来了解下ClassFile中的method_info,其格式如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
method_info里主要是一些方法信息:如访问标志、方法名索引、方法描述符索引及属性数组。这里要强调的是属性数组,因为字节码指令就存储在这个属性数组里。属性有很多种,比如说异常表就是一个属性,而存储字节码指令的属性为CODE属性,看这名字也知道是用来存储代码的了。属性的通用格式为:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
根据attribute_name_index可以从常量池中拿到属性名,再根据属性名就可以判断属性种类了。
Code属性的具体格式为:
Code_attribute {
u2 attribute_name_index; u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
其中code数组里存储就是字节码指令,那么如何解析呢?每条指令在code[]中都是一个字节,我们平时javap命令反编译看到的指令其实是助记符,只是方便阅读字节码使用的,jvm有一张字节码与助记符的对照表,根据对照表,就可以将指令翻译为可读的助记符了。这里我也是在网上随便找了一个对照表,保存到本地txt文件中,并在使用时解析成HashMap。代码很简单,就不贴了,可以参考我代码中InstructionTable.java。
接下来我们就可以解析字节码了:
for (int j = 0; j < methodInfo.attributesCount; j++) {
if (methodInfo.attributes[j] instanceof CodeAttribute) {
CodeAttribute codeAttribute = (CodeAttribute) methodInfo.attributes[j];
for (int m = 0; m < codeAttribute.codeLength; m++) {
short code = codeAttribute.code[m];
System.out.print(InstructionTable.getInstruction(code) + "\n");
}
}
}

运行

整个项目终于写完了,接下来就来看看效果如何,随便找一个class文件解析运行:
哈哈,是不是很赞!
由于篇幅限制,本文中只选取了一部分解析过程讲解,感兴趣的同学可参考我的github项目:https://github.com/HalfStackDeveloper/ClassReader,欢迎Fork And Star!

总结

Class文件看起来很复杂,其实真正解析起来,也没有那么难,关键是要自己动手试试,才能彻底理解,希望各位看完后也能觉知此事要躬行!

参考:

1. 周志明《java虚拟机规范(JavaSE7)》
2. 张秀宏《自己动手写Java虚拟机》
3. 周志明《深入理解Java虚拟机(第2版)》
(如有错误,欢迎指正!)
(转载请标明ID:半栈工程师,个人博客:https://halfstackdeveloper.github.io)
欢迎关注我的知乎专栏:https://zhuanlan.zhihu.com/halfstack
欢迎Follow我的github: https://github.com/HalfStackDeveloper


在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的。但是在通过了Apache,Squid,nginx等反向代理软件就不能获取到客户端的真实IP地址了。
如果使用了反向代理软件,将http://192.168.1.110:2046/ 的URL反向代理为 http://www.javapeixun.com.cn / 的URL时,用request.getRemoteAddr()方法获取的IP地址是:127.0.0.1 或 192.168.1.110,而并不是客户端的真实IP。
经过代理以后,由于在客户端和服务之间增加了中间层,因此服务器无法直接拿到客户端的IP,服务器端应用也无法直接通过转发请求的地址返回给客户端。但是在转发请求的HTTP头信息中,增加了X-FORWARDED-FOR信息。用以跟踪原有的客户端IP地址和原来客户端请求的服务器地址。当我们访问http://www.javapeixun.com.cn /index.jsp/ 时,其实并不是我们浏览器真正访问到了服务器上的index.jsp文件,而是先由代理服务器去访问http://192.168.1.110:2046/index.jsp ,代理服务器再将访问到的结果返回给我们的浏览器,因为是代理服务器去访问index.jsp的,所以index.jsp中通过request.getRemoteAddr()的方法获取的IP实际上是代理服务器的地址,并不是客户端的IP地址。
于是可得出获得客户端真实IP地址的方法一:
Java
public String getRemortIP(HttpServletRequest request) {
if (request.getHeader("x-forwarded-for") == null) {
return request.getRemoteAddr();
}
return request.getHeader("x-forwarded-for");
}
浏览器Flash插件异常,复制失败!
可是当我访问http://www.5a520.cn /index.jsp/ 时,返回的IP地址始终是unknown,也并不是如上所示的127.0.0.1 或 192.168.1.110了,而我访问http://192.168.1.110:2046/index.jsp 时,则能返回客户端的真实IP地址,写了个方法去验证。原因出在了Squid上。squid.conf 的配制文件 forwarded_for 项默认是为on,如果 forwarded_for 设成了 off  则:X-Forwarded-For: unknown
于是可得出获得客户端真实IP地址的方法二:
Java
import javax.servlet.http.HttpServletRequest;
/**
* 自定义访问对象工具类
*
* 获取对象的IP地址等信息
* @author X-rapido
*
*/
public class CusAccessObjectUtil {
/**
* 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
* 参考文章: http://developer.51cto.com/art/201111/305181.htm
*
* 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
* 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
*
* 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130,
* 192.168.1.100
*
* 用户真实IP为: 192.168.1.110
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
浏览器Flash插件异常,复制失败!
可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串Ip值,究竟哪个才是真正的用户端的真实IP呢?
答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100用户真实IP为: 192.168.1.110
除了上面的简单获取IP之外,一般的公司还会进行内网外网判断,完整示例如下
Java
import javax.servlet.http.HttpServletRequest;

/**
* IP地址工具类
* @author xudongdong
*
*/public class IpUtil {
/**
* 私有化构造器
*/
private IpUtil() {
}

/**
* 获取真实IP地址
* <p>使用getRealIP代替该方法</p>
* @param request req
* @return ip
*/
@Deprecated
public static String getClinetIpByReq(HttpServletRequest request) {
// 获取客户端ip地址
String clientIp = request.getHeader("x-forwarded-for");

if (clientIp == null || clientIp.length() == 0 || "unknown".equalsIgnoreCase(clientIp)) {
clientIp = request.getHeader("Proxy-Client-IP");
}
if (clientIp == null || clientIp.length() == 0 || "unknown".equalsIgnoreCase(clientIp)) {
clientIp = request.getHeader("WL-Proxy-Client-IP");
}
if (clientIp == null || clientIp.length() == 0 || "unknown".equalsIgnoreCase(clientIp)) {
clientIp = request.getRemoteAddr();
}
/*
* 对于获取到多ip的情况下,找到公网ip.
*/
String sIP = null;
if (clientIp != null && !clientIp.contains("unknown") && clientIp.indexOf(",") > 0) {
String[] ipsz = clientIp.split(",");
for (String anIpsz : ipsz) {
if (!isInnerIP(anIpsz.trim())) {
sIP = anIpsz.trim();
break;
}
}
/*
* 如果多ip都是内网ip,则取第一个ip.
*/
if (null == sIP) {
sIP = ipsz[0].trim();
}
clientIp = sIP;
}
if (clientIp != null && clientIp.contains("unknown")){
clientIp =clientIp.replaceAll("unknown,", "");
clientIp = clientIp.trim();
}
if ("".equals(clientIp) || null == clientIp){
clientIp = "127.0.0.1";
}
return clientIp;
}
/**
* 判断IP是否是内网地址
* @param ipAddress ip地址
* @return 是否是内网地址
*/
public static boolean isInnerIP(String ipAddress) {
boolean isInnerIp;
long ipNum = getIpNum(ipAddress);
/**
私有IP:A类 10.0.0.0-10.255.255.255
B类 172.16.0.0-172.31.255.255
C类 192.168.0.0-192.168.255.255
当然,还有127这个网段是环回地址
**/
long aBegin = getIpNum("10.0.0.0");
long aEnd = getIpNum("10.255.255.255");
long bBegin = getIpNum("172.16.0.0");
long bEnd = getIpNum("172.31.255.255");
long cBegin = getIpNum("192.168.0.0");
long cEnd = getIpNum("192.168.255.255");
isInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd)
|| ipAddress.equals("127.0.0.1");
return isInnerIp;
}

private static long getIpNum(String ipAddress) {
String[] ip = ipAddress.split("\\.");
long a = Integer.parseInt(ip[0]);
long b = Integer.parseInt(ip[1]);
long c = Integer.parseInt(ip[2]);
long d = Integer.parseInt(ip[3]);

return a * 256 * 256 * 256 + b * 256 * 256 + c * 256 + d;
}
private static boolean isInner(long userIp, long begin, long end) {
return (userIp >= begin) && (userIp <= end);
}

public static String getRealIP(HttpServletRequest request){
// 获取客户端ip地址
String clientIp = request.getHeader("x-forwarded-for");

if (clientIp == null || clientIp.length() == 0 || "unknown".equalsIgnoreCase(clientIp)) {
clientIp = request.getRemoteAddr();
}

String[] clientIps = clientIp.split(",");
if(clientIps.length <= 1) return clientIp.trim();

// 判断是否来自CDN
if(isComefromCDN(request)){
if(clientIps.length>=2) return clientIps[clientIps.length-2].trim();
}

return clientIps[clientIps.length-1].trim();
}

private static boolean isComefromCDN(HttpServletRequest request) {
String host = request.getHeader("host");
return host.contains("www.189.cn") ||host.contains("shouji.189.cn") || host.contains(
"image2.chinatelecom-ec.com") || host.contains(
"image1.chinatelecom-ec.com");
}}
原文地址:http://www.ibloger.net/article/144.html

jsr133之后增强了volatile语义,禁止了volatile变量与普通变量之间的重排序,根据volatile的happens-before原则,释放锁的线程在写volatile变量之前的共享变量(非volatile),在获取锁的线程读取同一个共享变量后将立即对获取锁的线程可见。
这儿实际上说法还是不完整,真正的还是依靠volatile+CAS,因为CAS基于CPU指令,intel中CAS操作带有lock前缀指令,该指令会将把写缓冲区中的所有数据刷新到内存中,读取volatile变量时可能还有其它的CPU指令来保证可见性。
所以我们姑且可以认为juc包中的锁的可见性是通过volatile类型的state变量和CAS操作实现的。

比如 下面一段代码就可以明白

private static Lock lock = new ReentrantLock();
private static int count = 0;
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10000; i++) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println(count);
count = count + 1;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
}
});
t1.start();
}
}
打印的结果是线程安全的,不存在跳过的可能。
而重入锁是通过volatile修饰的state实现的,如果一个普通常量是count,那么就相当于有两个变量:
int count;
volatile int state;
那么根据上面的内存语义释放锁的线程在写volatile变量之前的共享变量(非volatile),在获取锁的线程读取同一个共享变量后将立即对获取锁的线程可见可得count会对其他线程可见,所lock的可见性是aqs的volatile修饰的state带来的
----------参照《java并发编程的艺术3.5.3节》

JSR-133 规范,即 JavaTM内存模型与线程规范,由 JSR-133 专家组开发。 该规范是 JSR-176(定义了 JavaTM平台 Tiger(5.0)发布版的主要特性)的一部分。该规范的标准内容将合并到 JavaTM语言规范、JavaTM虚拟机规范以及 java.lang 包的类说明中。
JSR-133规范的核心:
1.通过内存屏障修正了volatile和final的重排序(final局部变量没有必要)的问题,从而保证了volatile变量的有序性和final初始化的有序性。
2.增强了volatile语义,增强volatile的可见性, 根据volatile的happens-before原则,释放锁的线程在写volatile变量之前的共享变量(非volatile),在获取锁的线程读取同一个共享变量后将立即对获取锁的线程可见(more可见 JUC包中的锁的可见性如何保证)。
关于JSR-133规范和翻译见附件:
jsr133.pdf
中文附件:
JSR133中文版1.pdf




一、为什么mysql innodb索引是B+树数据结构?言简意赅,就是因为:
1.文件很大,不可能全部存储在内存中,故要存储到磁盘上
2.索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数(为什么使用B-/+Tree,还跟磁盘存取原理有关。)
3.B+树所有的Data域在叶子节点,一般来说都会进行一个优化,就是将所有的叶子节点用指针串起来,这样遍历叶子节点就能获得全部数据。

二、什么是聚簇索引?
像innodb中,主键的索引结构中,既存储了主键值,又存储了行数据,这种结构称为”聚簇索引”

三、为什么MongoDB采用B树索引,而Mysql用B+树做索引
先从数据结构的角度来答。
题主应该知道B-树和B+树最重要的一个区别就是B+树只有叶节点存放数据,其余节点用来索引,而B-树是每个索引节点都会有Data域。
这就决定了B+树更适合用来存储外部数据,也就是所谓的磁盘数据。
从Mysql(Inoodb)的角度来看,B+树是用来充当索引的,一般来说索引非常大,尤其是关系性数据库这种数据量大的索引能达到亿级别,所以为了减少内存的占用,索引也会被存储在磁盘上。
那么Mysql如何衡量查询效率呢?磁盘IO次数,B-树(B类树)的特定就是每层节点数目非常多,层数很少,目的就是为了就少磁盘IO次数,当查询数据的时候,最好的情况就是很快找到目标索引,然后读取数据,使用B+树就能很好的完成这个目的,但是B-树的每个节点都有data域(指针),这无疑增大了节点大小,说白了增加了磁盘IO次数(磁盘IO一次读出的数据量大小是固定的,单个数据变大,每次读出的就少,IO次数增多,一次IO多耗时啊!),原因1:B+树除了叶子节点其它节点并不存储数据,节点小,磁盘IO次数就少。
原因2:B+树所有的Data域在叶子节点,一般来说都会进行一个优化,就是将所有的叶子节点用指针串起来。这样遍历叶子节点就能获得全部数据。


至于MongoDB为什么使用B-树而不是B+树,可以从它的设计角度来考虑,它并不是传统的关系性数据库,而是以Json格式作为存储的nosql,目的就是高性能,高可用,易扩展。首先它摆脱了关系模型,上面所述的优点2需求就没那么强烈了,其次Mysql由于使用B+树,数据都在叶节点上,每次查询都需要访问到叶节点,而MongoDB使用B-树,所有节点都有Data域,只要找到指定索引就可以进行访问,无疑单次查询平均快于Mysql(但侧面来看Mysql至少平均查询耗时差不多)。


总体来说,Mysql选用B+树和MongoDB选用B-树还是以自己的需求来选择的。

原文地址:http://blog.csdn.net/xuehuagongzi000/article/details/78985844

个人总结
消息队列的作用
  • 多个应用之间的完全解耦,由于消息是平台无关和语言无关的,而且语义上也不再是函数调用,因此更适合作为多个应用之间的松耦合的接口(在企业应用集成(EAI)中,文件传输,共享数据库,消息队列,远程过程调用都可以作为集成的方法)。
  • 跨系统的异步通信,所有需要异步交互的地方都可以使用消息队列。就像我们除了打电话(同步)以外,还需要发短信,发电子邮件(异步)的通讯方式。
  • 应用内的同步变异步,比如订单处理,就可以由前端应用将订单信息放到队列,后端应用从队列里依次获得消息处理,高峰时的大量订单可以积压在队列里慢慢处理掉。由于同步通常意味着阻塞,而大量线程的阻塞会降低计算机的性能。
  • 解决高并发瓶颈(削峰),这个福利来自于异步。
  • 广播,生产者可以将通过消息队列将任务交给多个消费者完成。
  • 消息驱动的架构(EDA),系统分解为消息队列,和消息制造者和消息消费者,一个处理流程可以根据需要拆成多个阶段(Stage),阶段之间用队列连接起来,前一个阶段处理的结果放入队列,后一个阶段从队列中获取消息继续处理。

消息队列的成本或者不足
1.维护消息队列,引入了一定的复杂度。
2.暂时的不一致性,消息队列是异步的,所以会存在暂时不一致。

消息队列的使用条件
1.生产者不需要从消费者处获得反馈
引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走——即所谓异步——成为了可能。
2.容许短暂的不一致性
  准许短暂的不一致性,但一定要保证最终的一致性。
3.确实是有一定效果
即解耦、提速、广播、削峰这些方面的收益,超过维护消息队列、监控消息队列这些成本。

知乎大牛的一个消息队列应用场景案例
假设用户在你的软件中注册,服务端收到用户的注册请求后,它会做这些操作:
  1. 校验用户名等信息,如果没问题会在数据库中添加一个用户记录
  2. 如果是用邮箱注册会给你发送一封注册成功的邮件,手机注册则会发送一条短信
  3. 分析用户的个人信息,以便将来向他推荐一些志同道合的人,或向那些人推荐他
  4. 发送给用户一个包含操作指南的系统通知
  5. 等等……
但是对于用户来说,注册功能实际只需要第一步,只要服务端将他的账户信息存到数据库中他便可以登录上去做他想做的事情了。至于其他的事情,非要在这一次请求中全部完成么?值得用户浪费时间等你处理这些对他来说无关紧要的事情么?所以实际当第一步做完后,服务端就可以把其他的操作放入对应的消息队列中然后马上返回用户结果,由消息队列异步的进行这些操作。
或者还有一种情况,同时有大量用户注册你的软件,再高并发情况下注册请求开始出现一些问题,例如邮件接口承受不住,或是分析信息时的大量计算使cpu满载,这将会出现虽然用户数据记录很快的添加到数据库中了,但是却卡在发邮件或分析信息时的情况,导致请求的响应时间大幅增长,甚至出现超时,这就有点不划算了。面对这种情况一般也是将这些操作放入消息队列(生产者消费者模型),消息队列慢慢的进行处理,同时可以很快的完成注册请求,不会影响用户使用其他功能。

知乎采摘
个人认为消息队列的主要特点是异步处理,主要目的是减少请求响应时间和解耦。所以主要的使用场景就是将比较耗时而且不需要即时(同步)返回结果的操作作为消息放入消息队列。同时由于使用了消息队列,只要保证消息格式不变,消息的发送方和接收方并不需要彼此联系,也不需要受对方的影响,即解耦和。
使用场景的话,举个例子:
假设用户在你的软件中注册,服务端收到用户的注册请求后,它会做这些操作:
  1. 校验用户名等信息,如果没问题会在数据库中添加一个用户记录
  2. 如果是用邮箱注册会给你发送一封注册成功的邮件,手机注册则会发送一条短信
  3. 分析用户的个人信息,以便将来向他推荐一些志同道合的人,或向那些人推荐他
  4. 发送给用户一个包含操作指南的系统通知
  5. 等等……
但是对于用户来说,注册功能实际只需要第一步,只要服务端将他的账户信息存到数据库中他便可以登录上去做他想做的事情了。至于其他的事情,非要在这一次请求中全部完成么?值得用户浪费时间等你处理这些对他来说无关紧要的事情么?所以实际当第一步做完后,服务端就可以把其他的操作放入对应的消息队列中然后马上返回用户结果,由消息队列异步的进行这些操作。
或者还有一种情况,同时有大量用户注册你的软件,再高并发情况下注册请求开始出现一些问题,例如邮件接口承受不住,或是分析信息时的大量计算使cpu满载,这将会出现虽然用户数据记录很快的添加到数据库中了,但是却卡在发邮件或分析信息时的情况,导致请求的响应时间大幅增长,甚至出现超时,这就有点不划算了。面对这种情况一般也是将这些操作放入消息队列(生产者消费者模型),消息队列慢慢的进行处理,同时可以很快的完成注册请求,不会影响用户使用其他功能。
所以在软件的正常功能开发中,并不需要去刻意的寻找消息队列的使用场景,而是当出现性能瓶颈时,去查看业务逻辑是否存在可以异步处理的耗时操作,如果存在的话便可以引入消息队列来解决。否则盲目的使用消息队列可能会增加维护和开发的成本却无法得到可观的性能提升,那就得不偿失了。
祁达方读书,写干货。
小红是小明的姐姐。

小红希望小明多读书,常寻找好书给小明看,之前的方式是这样:小红问小明什么时候有空,把书给小明送去,并亲眼监督小明读完书才走。久而久之,两人都觉得麻烦。

后来的方式改成了:小红对小明说「我放到书架上的书你都要看」,然后小红每次发现不错的书都放到书架上,小明则看到书架上有书就拿下来看。

书架就是一个消息队列,小红是生产者,小明是消费者。


这带来的好处有:

1.小红想给小明书的时候,不必问小明什么时候有空,亲手把书交给他了,小红只把书放到书架上就行了。这样小红小明的时间都更自由。

2.小红相信小明的读书自觉和读书能力,不必亲眼观察小明的读书过程,小红只要做一个放书的动作,很节省时间。

3.当明天有另一个爱读书的小伙伴小强加入,小红仍旧只需要把书放到书架上,小明和小强从书架上取书即可(唔,姑且设定成多个人取一本书可以每人取走一本吧,可能是拷贝电子书或复印,暂不考虑版权问题)。

4.书架上的书放在那里,小明阅读速度快就早点看完,阅读速度慢就晚点看完,没关系,比起小红把书递给小明并监督小明读完的方式,小明的压力会小一些。


这就是消息队列的四大好处:

1.解耦
每个成员不必受其他成员影响,可以更独立自主,只通过一个简单的容器来联系。
小红甚至可以不知道从书架上取书的是谁,小明也可以不知道往书架上放书的人是谁,在他们眼里,都只有书架,没有对方。
毫无疑问,与一个简单的容器打交道,比与复杂的人打交道容易一万倍,小红小明可以自由自在地追求各自的人生。

2.提速
小红选择相信「把书放到书架上,别的我不问」,为自己节省了大量时间。
小红很忙,只能抽出五分钟时间,但这时间足够把书放到书架上了。

3.广播
小红只需要劳动一次,就可以让多个小伙伴有书可读,这大大地节省了她的时间,也让新的小伙伴的加入成本很低。

4.削峰
假设小明读书很慢,如果采用小红每给一本书都监督小明读完的方式,小明有压力,小红也不耐烦。
反正小红给书的频率也不稳定,如果今明两天连给了五本,之后隔三个月才又给一本,那小明只要在三个月内从书架上陆续取走五本书读完就行了,压力就不那么大了。


当然,使用消息队列也有其成本:

1.引入复杂度
毫无疑问,「书架」这东西是多出来的,需要地方放它,还需要防盗。

2.暂时的不一致性
假如妈妈问小红「小明最近读了什么书」,在以前的方式里,小红因为亲眼监督小明读完书了,可以底气十足地告诉妈妈,但新的方式里,小红回答妈妈之后会心想「小明应该会很快看完吧……」
这中间存在着一段「妈妈认为小明看了某书,而小明其实还没看」的时期,当然,小明最终的阅读状态与妈妈的认知会是一致的,这就是所谓的「最终一致性」。


那么,该使用消息队列的情况需要满足什么条件呢?

1.生产者不需要从消费者处获得反馈
引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走——即所谓异步——成为了可能。
小红放完书之后小明到底看了没有,小红根本不问,她默认他是看了,否则就只能用原来的方法监督到看完了。

2.容许短暂的不一致性
妈妈可能会发现「有时候据说小明看了某书,但事实上他还没看」,只要妈妈满意于「反正他最后看了就行」,异步处理就没问题。
如果妈妈对这情况不能容忍,对小红大发雷霆,小红也就不敢用书架方式了。

3.确实是用了有效果
即解耦、提速、广播、削峰这些方面的收益,超过放置书架、监控书架这些成本。
否则如果是盲目照搬,「听说老赵家买了书架,咱们家也买一个」,买回来却没什么用,只是让步骤变多了,还不如直接把书递给对方呢,那就不对了。
用心阁软件工程师
  • 跨系统的异步通信,所有需要异步交互的地方都可以使用消息队列。就像我们除了打电话(同步)以外,还需要发短信,发电子邮件(异步)的通讯方式。
  • 多个应用之间的耦合,由于消息是平台无关和语言无关的,而且语义上也不再是函数调用,因此更适合作为多个应用之间的松耦合的接口。基于消息队列的耦合,不需要发送方和接收方同时在线。
    • 在企业应用集成(EAI)中,文件传输,共享数据库,消息队列,远程过程调用都可以作为集成的方法。
  • 应用内的同步变异步,比如订单处理,就可以由前端应用将订单信息放到队列,后端应用从队列里依次获得消息处理,高峰时的大量订单可以积压在队列里慢慢处理掉。由于同步通常意味着阻塞,而大量线程的阻塞会降低计算机的性能。
  • 消息驱动的架构(EDA),系统分解为消息队列,和消息制造者和消息消费者,一个处理流程可以根据需要拆成多个阶段(Stage),阶段之间用队列连接起来,前一个阶段处理的结果放入队列,后一个阶段从队列中获取消息继续处理。
  • 应用需要更灵活的耦合方式,如发布订阅,比如可以指定路由规则。
  • 跨局域网,甚至跨城市的通讯,比如北京机房与广州机房的应用程序的通信。
Doing微信公众号:EnjoyMoving
消息队列的一些应用场景:
  • 异步处理:
    • 非核心流程异步化,提高系统响应性能
  • 应用解耦:
    • 系统不是强耦合,消息接受者可以随意增加,而不需要修改消息发送者的代码。消息发送者的成功不依赖消息接受者(比如有些银行接口不稳定,但调用方并不需要依赖这些接口)
    • 不强依赖于非本系统的核心流程,对于非核心流程,可以放到消息队列中让消息消费者去按需消费,而不影响核心主流程
  • 最终一致性:最终一致性不是消息队列的必备特性,但确实可以依靠消息队列来做最终一致性的事情。
    • 先写消息再操作,确保操作完成后再修改消息状态。定时任务补偿机制实现消息可靠发送接收、业务操作的可靠执行,要注意消息重复与幂等设计
    • 所有不保证100%不丢消息的消息队列,理论上无法实现最终一致性。
  • 广播:
    • 只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情
  • 流量削峰与流控:
    • 当上下游系统处理能力存在差距的时候,利用消息队列做一个通用的“漏斗”。在下游有能力处理的时候,再进行分发。
  • 日志处理:
    • 将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题
  • 消息通讯:
    • 消息队列一般都内置了高效的通信机制,因此也可以用于单纯的消息通讯,比如实现点对点消息队列或者聊天室等。


对 dubbo 的协议的学习,可以知道目前主流 RPC 通信大概是什么情况,本文参考 dubbo 官方文档
http://dubbo.io/User+Guide-zh.htm
dubbo 共支持如下几种通信协议:
部分协议的特点和使用场景如下:
1、dubbo 协议
Dubbo 缺省协议采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。
缺省协议,使用基于mina1.1.7+hessian3.2.1的tbremoting交互。

连接个数:单连接
连接方式:长连接
传输协议:TCP
传输方式:NIO异步传输
序列化:Hessian二进制序列化
适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用dubbo协议传输大文件或超大字符串。
适用场景:常规远程服务方法调用

为什么要消费者比提供者个数多:
因dubbo协议采用单一长连接,
假设网络为千兆网卡(1024Mbit=128MByte),
根据测试经验数据每条连接最多只能压满7MByte(不同的环境可能不一样,供参考),
理论上1个服务提供者需要20个服务消费者才能压满网卡。

为什么不能传大包:
因dubbo协议采用单一长连接,
如果每次请求的数据包大小为500KByte,假设网络为千兆网卡(1024Mbit=128MByte),每条连接最大7MByte(不同的环境可能不一样,供参考),
单个服务提供者的TPS(每秒处理事务数)最大为:128MByte / 500KByte = 262。
单个消费者调用单个服务提供者的TPS(每秒处理事务数)最大为:7MByte / 500KByte = 14。
如果能接受,可以考虑使用,否则网络将成为瓶颈。

为什么采用异步单一长连接:
因为服务的现状大都是服务提供者少,通常只有几台机器,
而服务的消费者多,可能整个网站都在访问该服务,
比如Morgan的提供者只有6台提供者,却有上百台消费者,每天有1.5亿次调用,
如果采用常规的hessian服务,服务提供者很容易就被压跨,
通过单一连接,保证单一消费者不会压死提供者,
长连接,减少连接握手验证等,
并使用异步IO,复用线程池,防止C10K问题。
2、RMI
RMI 协议采用 JDK 标准的 java.rmi.*实现,采用阻塞式短连接和 JDK 标准序列化方式
Java 标准的远程调用协议。
连接个数:多连接
连接方式:短连接
传输协议:TCP
传输方式:同步传输
序列化:Java标准二进制序列化
适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。
适用场景:常规远程服务方法调用,与原生RMI服务互操作
3、hessian
Hessian 协议用于集成 Hessian 的服务,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现
基于Hessian的远程调用协议。

连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:Hessian二进制序列化
适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
适用场景:页面传输,文件传输,或与原生hessian服务互操作
4、http
采用 Spring 的 HttpInvoker 实现
基于 http 表单的远程调用协议。
连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:表单序列化(JSON
适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。
适用场景:需同时给应用程序和浏览器JS使用的服务。
5、webservice
基于 CXF 的 frontend-simple 和 transports-http 实现
基于WebService的远程调用协议。

连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:SOAP文本序列化
适用场景:系统集成,跨语言调用。
6、thrif
Thrift 是 Facebook 捐给 Apache 的一个 RPC 框架,当前 dubbo 支持的 thrift 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如 service name,magic number 等。

个人总结
前言
CAS即CompareAndSwap,是由底层提供的一个原子方法,这个方法模型中包括三个值,旧的内存值,内存值预期值,要设置的新内存值,当且仅当旧的内存值与内存值预期值相等时,才会成功将新的内存值写入到内存当中,写入成功返回true,写入失败返回false ,在java当中由UnSafe.compareAndSwapXXX方法提供CAS原语。

volatile关键字可以保证每次获取的字段值是内存当中的最新值(保证工作内存中该字段的值是无效的,这儿的工作内存是cpu高速缓存这些,如果是单核CPU,那就只有一个高速缓冲区,每个线程获取到的内存值都是一样,volatile就没有意义),volatile保证了多线程中共享变量的可见性,同时也禁止指令重排序,但不保证原子性。
注意:
   1.每个线程读写主内存的值时,都是要先把该值复制到工作内存,写完后,,再回写到主内存当中,什么时候写回去是不可控的,因此多个线程之间要共 享的变量必须保证可见性(监视器,juc锁,volatile,final都可以保证可见性)
  2.JSR-133加强了volatile的语义,释放锁的线程在写volatile变量之前的共享变量(非volatile),在获取锁的线程读取同一个共享变量后将立即对获取锁的线程可见。

CAS和volatile是java concurrent包的基础!整个concurrent包很少用到内置锁,任何阻塞线程不会进入Blocked状态,而是进入wating或者timewaiting,其通过类提供的锁或者同步容器,都是通过CAS,volatile和算法实现的,AQS是其中最重要的一个核心类。

concurrent包中提供的锁提供了可中断、可超时、可轮训、支持自旋的特性,比内置锁更为优秀,内置锁只能有一个等待队列,但对于concurrent包中的锁而言,一个condition就是一个条件队列,这个可以极大增加线程唤醒的正确性,降低锁资源的争夺和上下文的切换,但jvm开发小组一直在优化内置锁,提供了自旋锁、锁消除、锁粗化、轻量级锁等优秀特性且concurrent包中的锁更危险,因此只有内置锁满足不了需求时才应该使用cocurrent包中的锁

AQS即抽象队列同步(AbstractQueuedSynchronizer),其提供了多线程的资源的获取和释放的框架,可以实现线程同步,其中包括一个实现了Condition接口的ConditionObject内部类可以代替Object.notify(Condition.signal)和Object.wait(Condition.await)!

抽象队列同步的原理两句话
(1)AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(CLH)(多线程争用资源被阻塞时会进入此队列)。
   ConditonObject维护了一个单独的等待队列(注意这儿和CLH队列是另外一回事):
  AQS的CLH队列是用来对竞争锁资源的线程进行管理的队列,ConditionObject的队列是维护调用了await方法进入等待状态的线程的队列

(2)当一个线程视图获取AQS的资源时,如果获取失败,会被加入CLH队列,线程会进入等待状态(等待前会自旋),等待获取资源,当一个线程释放了AQS的资源时,会唤醒CLH中的等待队列的线程,被唤醒的线程会再试图获取资源.....
当一个线程调用conditon.await方法时,该线程会释放其所占有的资源,然后会被加入conditon的等待队列,并且唤醒AQS的CLH队列中的第一个线程,当调用condition.signal或者sianalAll时,会把Condion等待队列中的一个或者多个线程加入到AQS的CLH队列当中,从而让他们竞争资源

详细笔记
AQS关于资源的三个重要方法:
getState()//获取当前资源的数量
setState()//设置当前的资源数量
compareAndSetState()//使用CAS设置当前资源的方法
AQS关于获取和释放资源的核心方法
acquire(int):获取资源,框架已经实现,依靠tryAcquire
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。需要我们实现
release(int):释放独占资源 框架已经实现,依靠tryRelease
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。需要我们实现
acquireShared(int):共享方式获取资源,框架已经实现,依靠tryAcquireShared
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
releaseShared(int):共享方式释放资源,框架已经实现,依靠tryReleaseShared
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false
AQS关于判断和设置独占线程的方法(只能在独占模式下使用,或者独占共享混合模式)
//当前独占资源的线程
private transient Thread exclusiveOwnerThread;
//获取当前独占资源的线程
protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
 }
//设置当前独占资源的线程
 protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
 }
 //判断当前线程是否正在独占资源。只有用到condition才需要去实现它。贴合了notify和wait只能在同步块中使用的语法特性
 protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
      //一般这样实现 return getExclusiveOwnerThread() == Thread.currentThread();
}
Condition的重要方法
await:释放当前线程持有的AQS资源,被加入conditon等待队列,线程进入等待状态
signal:将conditon等待队列的第一个线程加入到AQS的CLH队列中等待获取资源
signalAll:将conditond等待队列的所有线程加入到AQS的CLH队列中等待获取资源

AQS定义了两种资源共享方式:
Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

Exclusive依靠acquire,tryAcquire,release,tryRelease:每个线程获取到资源后不会唤醒后面的线程,释放资源后只会唤醒后面的第一个线程。
  比如ReentrantLock是将state初始化为0,表示未锁定状态。当state等于0时线程可以获取到资源,然后会将state+1,当线程释放资源后又会将state-1,注意这儿是可以重入的,当state被重置为0时,其它线程才有机会获取该锁。要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

Share依靠acquireShared,tryAcquireShared,releaseShared,tryReleaseShared:每个线程获取到资源后会唤醒唤醒后面的线程,释放资源后会唤醒后面的多个线程,直到资源不够。
  比如CountDownLatch,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会获取到资源,从await()函数返回,继续后余动作。

不同的自定义同步器争用共享资源的方式不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false

一般来说,自定义同步器要么是独占方法,要么是共享方式,只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。当然,接口的实现要直接依赖sync,它们在语义上也存在某种对应关系!!而sync只用实现资源state的获取-释放方式tryAcquire-tryRelelase,至于线程的排队、等待、唤醒等,上层的AQS都已经实现好了,我们不用关心。

很多同步类都有公平和不公平的方式,唯一的区别在于获取资源(tryAcquire或者tryAcquireShared),非公平方式会直接获取资源,不管CLH队列是否为空。公平方式只有在CLH队列前没有等待线程时才会直接获取资源。非公平就可以让后来的线程有可能先获取到资源,不至于线程太饿,公平就会严格按照队列(非公平虽然获取资源是直接获取,不管队列是否为空,但是因为处于CLH的队列是挨着唤醒的,而且有检查,所以进入了CLH队列后就一定会排队了,简单来说非公平模式在第一次没获取到资源时,后面一定会按照顺序唤醒)

AQS还提供了有限时间等待,可中断的等待,前者不过是加了等待时间,后者只是注意了打断状态,其算法框架和思想还是和上面最基本的方式是一样的

整个AQS实现非常精妙,很有学习价值。勉强学通,后面慢慢深入

补充一个关于Condition的文章:http://m.blog.csdn.net/Truong/article/details/76473821#!/xh

AQS教程原文地址:http://www.cnblogs.com/waterystone/p/4920797.html

一、概述

  谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!
  类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch...。
  以下是本文的目录大纲:
  1. 概述
  2. 框架
  3. 源码详解
  4. 简单应用
  若有不正之处,请谅解和批评指正,不胜感激。
  请尊重作者劳动成果,转载请标明原文链接:http://www.cnblogs.com/waterystone/p/4920797.html
  手机版可访问:https://mp.weixin.qq.com/s/eyZyzk8ZzjwzZYN4a4H5YA  

二、框架

  它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:
  AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
  不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
  以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
  再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
  一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

三、源码详解

  本节开始讲解AQS的源码实现。依照acquire-release、acquireShared-releaseShared的次序来。

3.1 acquire(int)

  此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:
1 public final void acquire(int arg) {
2 if (!tryAcquire(arg) &&
3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4 selfInterrupt();
5 }
 
  函数流程如下:
  1. tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
  这时单凭这4个抽象的函数来看流程还有点朦胧,不要紧,看完接下来的分析后,你就会明白了。就像《大话西游》里唐僧说的:等你明白了舍生取义的道理,你自然会回来和我唱这首歌的。

3.1.1 tryAcquire(int)

  此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。这也正是tryLock()的语义,还是那句话,当然不仅仅只限于tryLock()。如下是tryAcquire()的源码:
1 protected boolean tryAcquire(int arg) {
2 throw new UnsupportedOperationException();
3 }
 
  什么?直接throw异常?说好的功能呢?好吧,还记得概述里讲的AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现吗?就是这里了!!!AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)!!!至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了!!!当然,自定义同步器在进行资源访问时要考虑线程安全的影响。
  这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。说到底,Doug Lea还是站在咱们开发者的角度,尽量减少不必要的工作量。

3.1.2 addWaiter(Node)

  此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。还是上源码吧:
1 private Node addWaiter(Node mode) {
2 //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
3 Node node = new Node(Thread.currentThread(), mode);
4
5 //尝试快速方式直接放到队尾。
6 Node pred = tail;
7 if (pred != null) {
8 node.prev = pred;
9 if (compareAndSetTail(pred, node)) {
10 pred.next = node;
11 return node;
12 }
13 }
14
15 //上一步失败则通过enq入队。
16 enq(node);
17 return node;
18 }
 不用再说了,直接看注释吧。

3.1.2.1 enq(Node)

   此方法用于将node加入队尾。源码如下:
1 private Node enq(final Node node) {
2 //CAS"自旋",直到成功加入队尾
3 for (;;) {
4 Node t = tail;
5 if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
6 if (compareAndSetHead(new Node()))
7 tail = head;
8 } else {//正常流程,放入队尾
9 node.prev = t;
10 if (compareAndSetTail(t, node)) {
11 t.next = node;
12 return t;
13 }
14 }
15 }
16 }
 
如果你看过AtomicInteger.getAndIncrement()函数源码,那么相信你一眼便看出这段代码的精华。CAS自旋volatile变量,是一种很经典的用法。还不太了解的,自己去百度一下吧。

3.1.3 acquireQueued(Node, int)

  OK,通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。聪明的你立刻应该能想到该线程下一部该干什么了吧:进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。没错,就是这样!是不是跟医院排队拿号有点相似~~acquireQueued()就是干这件事:在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。这个函数非常关键,还是上源码吧:
1 final boolean acquireQueued(final Node node, int arg) {
2 boolean failed = true;//标记是否成功拿到资源
3 try {
4 boolean interrupted = false;//标记等待过程中是否被中断过
5
6 //又是一个“自旋”!
7 for (;;) {
8 final Node p = node.predecessor();//拿到前驱
9 //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
10 if (p == head && tryAcquire(arg)) {
11 setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
12 p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
13 failed = false;
14 return interrupted;//返回等待过程中是否被中断过
15 }
16
17 //如果自己可以休息了,就进入waiting状态,直到被unpark()
18 if (shouldParkAfterFailedAcquire(p, node) &&
19 parkAndCheckInterrupt())
20 interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true21 }
22 } finally {
23 if (failed)
24 cancelAcquire(node);
25 }
26 }
 
到这里了,我们先不急着总结acquireQueued()的函数流程,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干些什么。

3.1.3.1 shouldParkAfterFailedAcquire(Node, Node)

  此方法主要用于检查状态,看看自己是否真的可以去休息了(进入waiting状态,如果线程状态转换不熟,可以参考本人上一篇写的Thread详解),万一队列前边的线程都放弃了只是瞎站着,那也说不定,对吧!
1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
2 int ws = pred.waitStatus;//拿到前驱的状态
3 if (ws == Node.SIGNAL)
4 //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
5 return true;
6 if (ws > 0) {
7 /*
8 * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
9 * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
10 */
11 do {
12 node.prev = pred = pred.prev;
13 } while (pred.waitStatus > 0);
14 pred.next = node;
15 } else {
16 //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
17 compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
18 }
19 return false;
20 }
 
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。

3.1.3.2 parkAndCheckInterrupt()

  如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。
1 private final boolean parkAndCheckInterrupt() {
2 LockSupport.park(this);//调用park()使线程进入waiting状态
3 return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
4 }
   park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。(再说一句,如果线程状态转换不熟,可以参考本人写的Thread详解)。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。 

3.1.3.3 小结

  OK,看了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),现在让我们再回到acquireQueued(),总结下该函数的具体流程:
  1. 结点进入队尾后,检查状态,找到安全休息点;
  2. 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
  3. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。
 

3.1.4 小结

  OKOK,acquireQueued()分析完之后,我们接下来再回到acquire()!再贴上它的源码吧:
1 public final void acquire(int arg) {
2 if (!tryAcquire(arg) &&
3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4 selfInterrupt();
5 }
再来总结下它的流程吧:
  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
由于此函数是重中之重,我再用流程图总结一下:
至此,acquire()的流程终于算是告一段落了。这也就是ReentrantLock.lock()的流程,不信你去看其lock()源码吧,整个函数就是一条acquire(1)!!!
 

3.2 release(int)

   上一小节已经把acquire()说完了,这一小节就来讲讲它的反操作release()吧。此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。下面是release()的源码:
1 public final boolean release(int arg) {
2 if (tryRelease(arg)) {
3 Node h = head;//找到头结点
4 if (h != null && h.waitStatus != 0)
5 unparkSuccessor(h);//唤醒等待队列里的下一个线程
6 return true;
7 }
8 return false;
9 }
 
  逻辑并不复杂。它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!

3.2.1 tryRelease(int)

  此方法尝试去释放指定量的资源。下面是tryRelease()的源码:
1 protected boolean tryRelease(int arg) {
2 throw new UnsupportedOperationException();
3 }
 
  跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。

3.2.2 unparkSuccessor(Node)

  此方法用于唤醒等待队列中下一个线程。下面是源码:
1 private void unparkSuccessor(Node node) {
2 //这里,node一般为当前线程所在的结点。
3 int ws = node.waitStatus;
4 if (ws < 0)//置零当前线程所在的结点状态,允许失败。
5 compareAndSetWaitStatus(node, ws, 0);
6
7 Node s = node.next;//找到下一个需要唤醒的结点s
8 if (s == null || s.waitStatus > 0) {//如果为空或已取消
9 s = null;
10 for (Node t = tail; t != null && t != node; t = t.prev)
11 if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
12 s = t;
13 }
14 if (s != null)
15 LockSupport.unpark(s.thread);//唤醒16 }
 
  这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!!And then, DO what you WANT!

3.2.3 小结

  release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

3.3 acquireShared(int)

  此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。下面是acquireShared()的源码:
1 public final void acquireShared(int arg) {
2 if (tryAcquireShared(arg) < 0)
3 doAcquireShared(arg);
4 }
 
  这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里acquireShared()的流程就是:
  1. tryAcquireShared()尝试获取资源,成功则直接返回;
  2. 失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。

3.3.1 doAcquireShared(int)

  此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。下面是doAcquireShared()的源码:
1 private void doAcquireShared(int arg) {
2 final Node node = addWaiter(Node.SHARED);//加入队列尾部
3 boolean failed = true;//是否成功标志
4 try {
5 boolean interrupted = false;//等待过程中是否被中断过的标志
6 for (;;) {
7 final Node p = node.predecessor();//前驱
8 if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
9 int r = tryAcquireShared(arg);//尝试获取资源
10 if (r >= 0) {//成功
11 setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
12 p.next = null; // help GC
13 if (interrupted)//如果等待过程中被打断过,此时将中断补上。
14 selfInterrupt();
15 failed = false;
16 return;
17 }
18 }
19
20 //判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
21 if (shouldParkAfterFailedAcquire(p, node) &&
22 parkAndCheckInterrupt())
23 interrupted = true;
24 }
25 } finally {
26 if (failed)
27 cancelAcquire(node);
28 }
29 }
 
  有木有觉得跟acquireQueued()很相似?对,其实流程并没有太大区别。只不过这里将补中断的selfInterrupt()放到doAcquireShared()里了,而独占模式是放到acquireQueued()之外,其实都一样,不知道Doug Lea是怎么想的。
  跟独占模式比,还有一点需要注意的是,这里只有线程是head.next时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。那么问题就来了,假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。老大先唤醒老二,老二一看资源不够,他是把资源让给老三呢,还是不让?答案是否定的!老二会继续park()等待其他线程释放资源,也更不会去唤醒老三和老四了。独占模式,同一时刻只有一个线程去执行,这样做未尝不可;但共享模式下,多个线程是可以同时执行的,现在因为老二的资源需求量大,而把后面量小的老三和老四也都卡住了。当然,这并不是问题,只是AQS保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)。
 

3.3.1.1 setHeadAndPropagate(Node, int)

1 private void setHeadAndPropagate(Node node, int propagate) {
2 Node h = head;
3 setHead(node);//head指向自己
4 //如果还有剩余量,继续唤醒下一个邻居线程
5 if (propagate > 0 || h == null || h.waitStatus < 0) {
6 Node s = node.next;
7 if (s == null || s.isShared())
8 doReleaseShared();
9 }
10 }
 
  此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!
  doReleaseShared()我们留着下一小节的releaseShared()里来讲。
 

3.3.2 小结

  OK,至此,acquireShared()也要告一段落了。让我们再梳理一下它的流程:
  1. tryAcquireShared()尝试获取资源,成功则直接返回;
  2. 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。
  其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)

3.4 releaseShared()

  上一小节已经把acquireShared()说完了,这一小节就来讲讲它的反操作releaseShared()吧。此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。下面是releaseShared()的源码:
1 public final boolean releaseShared(int arg) {
2 if (tryReleaseShared(arg)) {//尝试释放资源
3 doReleaseShared();//唤醒后继结点
4 return true;
5 }
6 return false;
7 }
 
  此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。

3.4.1 doReleaseShared()

  此方法主要用于唤醒后继。下面是它的源码:
1 private void doReleaseShared() {
2 for (;;) {
3 Node h = head;
4 if (h != null && h != tail) {
5 int ws = h.waitStatus;
6 if (ws == Node.SIGNAL) {
7 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
8 continue;
9 unparkSuccessor(h);//唤醒后继
10 }
11 else if (ws == 0 &&
12 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
13 continue;
14 }
15 if (h == head)// head发生变化
16 break;
17 }
18 }
 
 

3.5 小结

  本节我们详解了独占和共享两种模式下获取-释放资源(acquire-release、acquireShared-releaseShared)的源码,相信大家都有一定认识了。值得注意的是,acquire()和acquireSahred()两种方法下,线程在等待队列中都是忽略中断的。AQS也支持响应中断的,acquireInterruptibly()/acquireSharedInterruptibly()即是,这里相应的源码跟acquire()和acquireSahred()差不多,这里就不再详解了。
 

四、简单应用

  通过前边几个章节的学习,相信大家已经基本理解AQS的原理了。这里再将“框架”一节中的一段话复制过来:
  不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
  OK,下面我们就以AQS源码里的Mutex为例,讲一下AQS的简单应用。

4.1 Mutex(互斥锁)

  Mutex是一个不可重入的互斥锁实现。锁资源(AQS里的state)只有两种状态:0表示未锁定,1表示锁定。下边是Mutex的核心源码:
1 class Mutex implements Lock, java.io.Serializable {
2 // 自定义同步器
3 private static class Sync extends AbstractQueuedSynchronizer {
4 // 判断是否锁定状态
5 protected boolean isHeldExclusively() {
6 return getState() == 1;
7 }
8
9 // 尝试获取资源,立即返回。成功则返回true,否则false。
10 public boolean tryAcquire(int acquires) {
11 assert acquires == 1; // 这里限定只能为1个量
12 if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
13 setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
14 return true;
15 }
16 return false;
17 }
18
19 // 尝试释放资源,立即返回。成功则为true,否则false。
20 protected boolean tryRelease(int releases) {
21 assert releases == 1; // 限定为1个量
22 if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
23 throw new IllegalMonitorStateException();
24 setExclusiveOwnerThread(null);
25 setState(0);//释放资源,放弃占有状态26 return true;
27 }
28 }
29
30 // 真正同步类的实现都依赖继承于AQS的自定义同步器!
31 private final Sync sync = new Sync();
32
33 //lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
34 public void lock() {
35 sync.acquire(1);
36 }
37
38 //tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
39 public boolean tryLock() {
40 return sync.tryAcquire(1);
41 }
42
43 //unlock<-->release。两者语文一样:释放资源。
44 public void unlock() {
45 sync.release(1);
46 }
47
48 //锁是否占有状态
49 public boolean isLocked() {
50 return sync.isHeldExclusively();
51 }
52 }
 
  同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。当然,接口的实现要直接依赖sync,它们在语义上也存在某种对应关系!!而sync只用实现资源state的获取-释放方式tryAcquire-tryRelelase,至于线程的排队、等待、唤醒等,上层的AQS都已经实现好了,我们不用关心。
  除了Mutex,ReentrantLock/CountDownLatch/Semphore这些同步类的实现方式都差不多,不同的地方就在获取-释放资源的方式tryAcquire-tryRelelase。掌握了这点,AQS的核心便被攻破了!
  OK,至此,整个AQS的讲解也要落下帷幕了。希望本文能够对学习Java并发编程的同学有所借鉴,中间写的有不对的地方,也欢迎讨论和指正~
作者:水岩
    
出处:http://www.cnblogs.com/waterystone/
    
本博客中未标明转载的文章归作者水岩和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。


个人理解
ThreadLocal版本变化和源码剖析
Java Version SE 6
ThreadLocal取消使用SynchronizedMap来存各个线程的变量副本,而是在每个Thread中有一个ThreadLocalMap的变量,然后用Thread.currentThread().threadLocals.get(ThreadLocal)来引用的各线程变量副本,这样避免了去同步全局的Map。简单来说ThreadLocal对象相当于线程私有财产(数据)的经纪人,为线程的私有财产(数据)提供存储和获取的功能
什么好处呢:
1.可以减少同步!访问ThreadLocal不需要任何的同步机制 
2.当一个线程消亡,那么该线程所绑定的ThreadLocal局部变量也会跟着被回收,简单粗暴
注意:Thread.currentThread().threadLocals.get(ThreadLocal)没有直接使用ThreadLocal的hashcode,而是一个自定义的hashcode,每个ThreadLocal的HashCode都带有固定间隔
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

Java Version SE 5
对ThreadLocal增加remove方法,增加泛型支持

J2SE Version 1.2
推出ThreadLocal的初步API,只包括initialValue,get,set三个方法,实现是一个ThreadLocal内部维护一个SynchronizedMap,key为当前的Thread!
public class ThreadLocal{
private Map values = Collections.synchronizedMap(new HashMap());
public Object get(){
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
if (o == null && !values.containsKey(curThread)){
o = initialValue();
values.put(curThread, o);
}
values.put(Thread.currentThread(), newValue);
return o ;
}
public Object initialValue(){
return null;
}
}
在这种实现模式下,ThreadLocal实际上就是一个SynchronizedMap的包装

老版ThreadLocal的内存泄露问题
每个ThreadLocal内部维护一个SynchronizedMap,key为当前的Thread,所以当Thread在其它地方的没有引用的时候,但在ThreadLocal当中也还会引用Thread,从而导致Thread对象无法回收。
 
新版ThreadLocal的内存泄露问题
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个ThreadLocal实例引用. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向ThreadLocal. 当把ThreadLocal实例置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.如图:

PS.Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value(expungeStaleEntries方法,但这个设计得并不完全,没有使用引用队列)。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。


修改措施
1.老版本的ThreadLocal可以使用被同步装饰的WeakHashMap来解决泄漏问题。
eg:Collections.synchronizedMap(new WeakHashMap<Thread, T>())
但是这种泄漏的修复,必须要调用get和set等方法才能去移除旧值
2.新版本内存泄漏并不是一直存在,会有主动的检查,但是如果所有的ThreadLocal被置为了null(一般不可能),那么这种检查措施也不会被调用。考虑过使用WeakHashMap的原理,使用引用队列来实现expungeStaleEntries方法,但这个方法还是只能在get和set中调用,也无法解决这个问题。最怕的还是TheadLoal放置为null,还使用的是线程池。
 和上面的情况是类似的问题
原文
深入研究java.lang.ThreadLocal类
 
 
一、概述
 
ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
 
从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
 
通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
 
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
 
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
 
二、API说明
 
ThreadLocal()
          创建一个线程本地变量。
 
T get()
          返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
 
protected  T initialValue()
          返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。
 
   若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。
 
void remove()
          移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。
 
void set(T value)
          将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。
 
在程序中一般都重写initialValue方法,以给定一个特定的初始值。
 
 
三、典型实例
 
1、Hiberante的Session 工具类HibernateUtil
这个类是Hibernate官方文档中HibernateUtil类,用于session管理。
 
public class HibernateUtil {
    private static Log log = LogFactory.getLog(HibernateUtil.class);
    private static final SessionFactory sessionFactory;     //定义SessionFactory
 
    static {
        try {
            // 通过默认配置文件hibernate.cfg.xml创建SessionFactory
            sessionFactory = new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            log.error("初始化SessionFactory失败!", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    //创建线程局部变量session,用来保存Hibernate的Session
    public static final ThreadLocal session = new ThreadLocal();
 
    /**
     * 获取当前线程中的Session
     * @return Session
     * @throws HibernateException
     */
    public static Session currentSession() throws HibernateException {
        Session s = (Session) session.get();
        // 如果Session还没有打开,则新开一个Session
        if (s == null) {
            s = sessionFactory.openSession();
            session.set(s);         //将新开的Session保存到线程局部变量中
        }
        return s;
    }
 
    public static void closeSession() throws HibernateException {
        //获取线程局部变量,并强制转换为Session类型
        Session s = (Session) session.get();
        session.set(null);
        if (s != null)
            s.close();
    }
}
 
在这个类中,由于没有重写ThreadLocal的initialValue()方法,则首次创建线程局部变量session其初始值为null,第一次调用currentSession()的时候,线程局部变量的get()方法也为null。因此,对session做了判断,如果为null,则新开一个Session,并保存到线程局部变量session中,这一步非常的关键,这也是“public static final ThreadLocal session = new ThreadLocal()”所创建对象session能强制转换为Hibernate Session对象的原因。
 
2、另外一个实例
创建一个Bean,通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性。
 
/**
 * Created by IntelliJ IDEA.
 * User: leizhimin
 * Date: 2007-11-23
 * Time: 10:45:02
 * 学生
 */
public class Student {
    private int age = 0;   //年龄
 
    public int getAge() {
        return this.age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
}
 
/**
 * Created by IntelliJ IDEA.
 * User: leizhimin
 * Date: 2007-11-23
 * Time: 10:53:33
 * 多线程下测试程序
 */
public class ThreadLocalDemo implements Runnable {
    //创建线程局部变量studentLocal,在后面你会发现用来保存Student对象
    private final static ThreadLocal studentLocal = new ThreadLocal();
 
    public static void main(String[] agrs) {
        ThreadLocalDemo td = new ThreadLocalDemo();
        Thread t1 = new Thread(td, "a");
        Thread t2 = new Thread(td, "b");
        t1.start();
        t2.start();
    }
 
    public void run() {
        accessStudent();
    }
 
    /**
     * 示例业务方法,用来测试
     */
    public void accessStudent() {
        //获取当前线程的名字
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running!");
        //产生一个随机数并打印
        Random random = new Random();
        int age = random.nextInt(100);
        System.out.println("thread " + currentThreadName + " set age to:" + age);
        //获取一个Student对象,并将随机数年龄插入到对象属性中
        Student student = getStudent();
        student.setAge(age);
        System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
    }
 
    protected Student getStudent() {
        //获取本地线程变量并强制转换为Student类型
        Student student = (Student) studentLocal.get();
        //线程首次执行此方法的时候,studentLocal.get()肯定为null
        if (student == null) {
            //创建一个Student对象,并保存到本地线程变量studentLocal中
            student = new Student();
            studentLocal.set(student);
        }
        return student;
    }
}
 
运行结果:
a is running! 
thread a set age to:76 
b is running! 
thread b set age to:27 
thread a first read age is:76 
thread b first read age is:27 
thread a second read age is:76 
thread b second read age is:27 
 
可以看到a、b两个线程age在不同时刻打印的值是完全相同的。这个程序通过妙用ThreadLocal,既实现多线程并发,游兼顾数据的安全性。
 
四、总结
 
ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
 
ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。
 
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
 
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
 
当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。
 
 
五、ThreadLocal使用的一般步骤
 
1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
 
 
参考文档:
JDK 官方文档
[url]http://www.java3z.com/cwbwebhome/article/article2a/271.html?id=319[/url]
[url]http://www.java3z.com/cwbwebhome/article/article2/2952.html?id=1648[/url]

Java进阶(七)正确理解Thread Local的原理与适用场景

ThreadLocal解决什么问题

由于 ThreadLocal 支持范型,如 ThreadLocal< StringBuilder >,为表述方便,后文用 变量 代表 ThreadLocal 本身,而用 实例 代表具体类型(如 StringBuidler )的实例。

不恰当的理解

写这篇文章的一个原因在于,网上很多博客关于 ThreadLocal 的适用场景以及解决的问题,描述的并不清楚,甚至是错的。下面是常见的对于 ThreadLocal的介绍
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路
ThreadLocal的目的是为了解决多线程访问资源时的共享问题
还有很多文章在对比 ThreadLocal 与 synchronize 的异同。既然是作比较,那应该是认为这两者解决相同或类似的问题。
上面的描述,问题在于,ThreadLocal 并不解决多线程 共享 变量的问题。既然变量不共享,那就更谈不上同步的问题。

合理的理解

ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的 Thread 中有不同的副本(实际是不同的实例,后文会详细阐述)。这里有几点需要注意
那 ThreadLocal 到底解决了什么问题,又适用于什么样的场景?
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
核心意思是
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。后文会通过实例详细阐述该观点。另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁。

ThreadLocal用法

实例代码

下面通过如下代码说明 ThreadLocal 的使用方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
int threads = 3;
CountDownLatch countDownLatch = new CountDownLatch(threads);
InnerClass innerClass = new InnerClass();
for(int i = 1; i <= threads; i++) {
new Thread(() -> {
for(int j = 0; j < 4; j++) {
innerClass.add(String.valueOf(j));
innerClass.print();
}
innerClass.set("hello world");
countDownLatch.countDown();
}, "thread - " + i).start();
}
countDownLatch.await();
}
private static class InnerClass {
public void add(String newStr) {
StringBuilder str = Counter.counter.get();
Counter.counter.set(str.append(newStr));
}
public void print() {
System.out.printf("Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",
Thread.currentThread().getName(),
Counter.counter.hashCode(),
Counter.counter.get().hashCode(),
Counter.counter.get().toString());
}
public void set(String words) {
Counter.counter.set(new StringBuilder(words));
System.out.printf("Set, Thread name:%s , ThreadLocal hashcode:%s, Instance hashcode:%s, Value:%s\n",
Thread.currentThread().getName(),
Counter.counter.hashCode(),
Counter.counter.get().hashCode(),
Counter.counter.get().toString());
}
}
private static class Counter {
private static ThreadLocal<StringBuilder> counter = new ThreadLocal<StringBuilder>() {
@Override
protected StringBuilder initialValue() {
return new StringBuilder();
}
};
}
}

实例分析

ThreadLocal本身支持范型。该例使用了 StringBuilder 类型的 ThreadLocal 变量。可通过 ThreadLocal 的 get() 方法读取 StringBuidler 实例,也可通过 set(T t) 方法设置 StringBuilder。
上述代码执行结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:0
Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:0
Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:0
Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:01
Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:01
Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:012
Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1609588821, Value:0123
Set, Thread name:thread - 3 , ThreadLocal hashcode:372282300, Instance hashcode:1362597339, Value:hello world
Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:01
Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:012
Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:012
Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:418873098, Value:0123
Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1780437710, Value:0123
Set, Thread name:thread - 1 , ThreadLocal hashcode:372282300, Instance hashcode:482932940, Value:hello world
Set, Thread name:thread - 2 , ThreadLocal hashcode:372282300, Instance hashcode:1691922941, Value:hello world
从上面的输出可看出

ThreadLocal原理

ThreadLocal维护线程与实例的映射

既然每个访问 ThreadLocal 变量的线程都有自己的一个“本地”实例副本。一个可能的方案是 ThreadLocal 维护一个 Map,键是 Thread,值是它在该 Thread 内的实例。线程通过该 ThreadLocal 的 get() 方案获取实例时,只需要以线程为键,从 Map 中找出对应的实例即可。该方案如下图所示

该方案可满足上文提到的每个线程内一个独立备份的要求。每个新线程访问该 ThreadLocal 时,需要向 Map 中添加一个映射,而每个线程结束时,应该清除该映射。这里就有两个问题:
其中锁的问题,是 JDK 未采用该方案的一个原因。

Thread维护ThreadLocal与实例的映射

上述方案中,出现锁的问题,原因在于多线程访问同一个 Map。如果该 Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。该方案如下图所示。

该方案虽然没有锁的问题,但是由于每个线程访问某 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),则这些 ThreadLocal 不能被回收,可能会造成内存泄漏。后文会介绍 JDK 如何解决该问题。

ThreadLocal 在 JDK 8 中的实现

ThreadLocalMap与内存泄漏

该方案中,Map 由 ThreadLocal 类的静态内部类 ThreadLocalMap 提供。该类的实例维护某个 ThreadLocal 与具体实例的映射。与 HashMap 不同的是,ThreadLocalMap 的每个 Entry 都是一个对  的弱引用,这一点从super(k)可看出。另外,每个 Entry 都包含了一个对  的强引用。
1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
使用弱引用的原因在于,当没有强引用指向 ThreadLocal 变量时,它可被回收,从而避免上文所述 ThreadLocal 不能被回收而造成的内存泄漏的问题。
但是,这里又可能出现另外一种内存泄漏的问题。ThreadLocalMap 维护 ThreadLocal 变量与具体实例的映射,当 ThreadLocal 变量被回收后,该映射的键变为 null,该 Entry 无法被移除。从而使得实例被该 Entry 引用而无法被回收造成内存泄漏。
注:Entry虽然是弱引用,但它是 ThreadLocal 类型的弱引用(也即上文所述它是对  的弱引用),而非具体实例的的弱引用,所以无法避免具体实例相关的内存泄漏。

读取实例

读取实例方法如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
读取实例时,线程首先通过getMap(t)方法获取自身的 ThreadLocalMap。从如下该方法的定义可见,该 ThreadLocalMap 的实例是 Thread 类的一个字段,即由 Thread 维护 ThreadLocal 对象与具体实例的映射,这一点与上文分析一致。
1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
获取到 ThreadLocalMap 后,通过map.getEntry(this)方法获取该 ThreadLocal 在当前线程的 ThreadLocalMap 中对应的 Entry。该方法中的 this 即当前访问的 ThreadLocal 对象。
如果获取到的 Entry 不为 null,从 Entry 中取出值即为所需访问的本线程对应的实例。如果获取到的 Entry 为 null,则通过setInitialValue()方法设置该 ThreadLocal 变量在该线程中对应的具体实例的初始值。

设置初始值

设置初始值方法如下
1
2
3
4
5
6
7
8
9
10
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
该方法为 private 方法,无法被重载。
首先,通过initialValue()方法获取初始值。该方法为 public 方法,且默认返回 null。所以典型用法中常常重载该方法。上例中即在内部匿名类中将其重载。
然后拿到该线程对应的 ThreadLocalMap 对象,若该对象不为 null,则直接将该 ThreadLocal 对象与对应实例初始值的映射添加进该线程的 ThreadLocalMap中。若为 null,则先创建该 ThreadLocalMap 对象再将映射添加其中。
这里并不需要考虑 ThreadLocalMap 的线程安全问题。因为每个线程有且只有一个 ThreadLocalMap 对象,并且只有该线程自己可以访问它,其它线程不会访问该 ThreadLocalMap,也即该对象不会在多个线程中共享,也就不存在线程安全的问题。

设置实例

除了通过initialValue()方法设置实例的初始值,还可通过 set 方法设置线程内实例的值,如下所示。
1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
该方法先获取该线程的 ThreadLocalMap 对象,然后直接将 ThreadLocal 对象(即代码中的 this)与目标实例的映射添加进 ThreadLocalMap 中。当然,如果映射已经存在,就直接覆盖。另外,如果获取到的 ThreadLocalMap 为 null,则先创建该 ThreadLocalMap 对象。

防止内存泄漏

对于已经不再被使用且已被回收的 ThreadLocal 对象,它在每个线程内对应的实例由于被线程的 ThreadLocalMap 的 Entry 强引用,无法被回收,可能会造成内存泄漏。
针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

适用场景

如上文所述,ThreadLocal 适用于如下两种场景
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

案例

对于 Java Web 应用而言,Session 保存了很多信息。很多时候需要通过 Session 获取信息,有些时候又需要修改 Session 的信息。一方面,需要保证每个线程有自己单独的 Session 实例。另一方面,由于很多地方都需要操作 Session,存在多方法共享 Session 的需求。如果不使用 ThreadLocal,可以在每个线程内构建一个 Session实例,并将该实例在多个方法间传递,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SessionHandler {
@Data
public static class Session {
private String id;
private String user;
private String status;
}
public Session createSession() {
return new Session();
}
public String getUser(Session session) {
return session.getUser();
}
public String getStatus(Session session) {
return session.getStatus();
}
public void setStatus(Session session, String status) {
session.setStatus(status);
}
public static void main(String[] args) {
new Thread(() -> {
SessionHandler handler = new SessionHandler();
Session session = handler.createSession();
handler.getStatus(session);
handler.getUser(session);
handler.setStatus(session, "close");
handler.getStatus(session);
}).start();
}
}
该方法是可以实现需求的。但是每个需要使用 Session 的地方,都需要显式传递 Session 对象,方法间耦合度较高。
这里使用 ThreadLocal 重新实现该功能如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SessionHandler {
public static ThreadLocal<Session> session = new ThreadLocal<Session>();
@Data
public static class Session {
private String id;
private String user;
private String status;
}
public void createSession() {
session.set(new Session());
}
public String getUser() {
return session.get().getUser();
}
public String getStatus() {
return session.get().getStatus();
}
public void setStatus(String status) {
session.get().setStatus(status);
}
public static void main(String[] args) {
new Thread(() -> {
SessionHandler handler = new SessionHandler();
handler.getStatus();
handler.getUser();
handler.setStatus("close");
handler.getStatus();
}).start();
}
}
使用 ThreadLocal 改造后的代码,不再需要在各个方法间传递 Session 对象,并且也非常轻松的保证了每个线程拥有自己独立的实例。
如果单看其中某一点,替代方法很多。比如可通过在线程内创建局部变量可实现每个线程有自己的实例,使用静态变量可实现变量在方法间的共享。但如果要同时满足变量在线程间的隔离与方法间的共享,ThreadLocal再合适不过。

总结

Java进阶系列


学习文章
1.Thread类中的threadLocals 和ThreadLocal原理:http://www.iteye.com/topic/1141743
2.ThreadLocal可能引起的内存泄露:https://www.cnblogs.com/onlywujun/p/3524675.html
3.深入研究java.lang.ThreadLocal类:http://lavasoft.blog.51cto.com/62575/51926/
4.Java进阶(七)正确理解Thread Local的原理与适用场景 http://www.jasongj.com/java/threadlocal/


看了一堆文章,终于把Java CAS的原理深入分析清楚了。
感谢GOOGLE强大的搜索,借此挖苦下百度,依靠百度什么都学习不到!
 
参考文档:
http://www.blogjava.NET/xylz/archive/2010/07/04/325206.html
http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html
 
java.util.concurrent包完全建立在CAS之上的,没有CAS就不会有此包。可见CAS的重要性。
 
CAS
CAS:Compare and Swap, 翻译成比较并交换。 
java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。
 
本文先从CAS的应用说起,再深入原理解析。
 
CAS应用
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
 
非阻塞算法 (nonblocking algorithms)
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。
拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。
private volatile int value;
首先毫无以为,在没有锁的机制下可能需要借助volatile原语,保证线程间的数据是可见的(共享的)。
这样才获取变量的值的时候才能直接读取。
public final int get() {
        return value;
    }
然后来看看++i是怎么做到的。
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
在这里采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
而compareAndSet利用JNI来完成CPU指令的操作。
public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。
 
其中
unsafe.compareAndSwapInt(this, valueOffset, expect, update);
类似:
if (this == expect) {
  this = update
 return true;
} else {
return false;
}
 
那么问题就来了,成功过程中需要2个步骤:比较this == expect,替换this = update,compareAndSwapInt如何这两个步骤的原子性呢? 参考CAS的原理。
 
CAS原理
 CAS通过调用JNI的代码实现的。JNI:java Native Interface为JAVA本地调用,允许java调用其他语言。
而compareAndSwapInt就是借助C来调用CPU底层指令实现的。
下面从分析比较常用的CPU(intel x86)来解释CAS的实现原理。
 下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
 
可以看到这是个本地方法调用。这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。这个本地方法的最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(对应于windows操作系统,X86处理器)。下面是对应于intel x86处理器的源代码的片段:
 
// Adding a lock prefix to an instruction on MP machine// VC++ doesn't like the lock prefix to be on a single line// so we can't insert a label after the lock prefix.// By emitting a lock prefix, we can define a label after it.#define LOCK_IF_MP(mp) __asm cmp mp, 0 \ __asm je L0 \ __asm _emit 0xF0 \ __asm L0:inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx }}
如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
 
 intel的手册对lock前缀的说明如下:
  1. 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
  2. 禁止该指令与之前和之后的读和写指令重排序。
  3. 把写缓冲区中的所有数据刷新到内存中。
备注知识:
关于CPU的锁有如下3种:
  3.1 处理器自动保证基本内存操作的原子性
  首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。 
  3.2 使用总线锁保证原子性
  第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。如下图
 
 
  原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。
  处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
  3.3 使用缓存锁保证原子性
  第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
  频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。
  但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
  以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。
 
CAS缺点
 CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作
1.  ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
关于ABA问题参考文档: http://blog.hesey.Net/2011/09/resolve-aba-by-atomicstampedreference.html
2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
 
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
 
 

concurrent包的实现

由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:
  1. A线程写volatile变量,随后B线程读这个volatile变量。
  2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
  3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  4. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:



理解HTTP幂等性

基于HTTP协议的Web API是时下最为流行的一种分布式服务提供方式。无论是在大型互联网应用还是企业级架构中,我们都见到了越来越多的SOA或RESTful的Web API。为什么Web API如此流行呢?我认为很大程度上应归功于简单有效的HTTP协议。HTTP协议是一种分布式的面向资源的网络应用层协议,无论是服务器端提供Web服务,还是客户端消费Web服务都非常简单。再加上浏览器、Javascript、AJAX、JSON以及HTML5等技术和工具的发展,互联网应用架构设计表现出了从传统的PHP、JSP、ASP.NET等服务器端动态网页向Web API + RIA(富互联网应用)过渡的趋势。Web API专注于提供业务服务,RIA专注于用户界面和交互设计,从此两个领域的分工更加明晰。在这种趋势下,Web API设计将成为服务器端程序员的必修课。然而,正如简单的Java语言并不意味着高质量的Java程序,简单的HTTP协议也不意味着高质量的Web API。要想设计出高质量的Web API,还需要深入理解分布式系统及HTTP协议的特性。

幂等性定义

本文所要探讨的正是HTTP协议涉及到的一种重要性质:幂等性(Idempotence)。在HTTP/1.1规范中幂等性的定义是:
Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.
从定义上看,HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。幂等性属于语义范畴,正如编译器只能帮助检查语法错误一样,HTTP规范也没有办法通过消息格式等语法手段来定义它,这可能是它不太受到重视的原因之一。但实际上,幂等性是分布式系统设计中十分重要的概念,而HTTP的分布式本质也决定了它在HTTP中具有重要地位。

分布式事务 vs 幂等设计

为什么需要幂等性呢?我们先从一个例子说起,假设有一个从账户取钱的远程API(可以是HTTP的,也可以不是),我们暂时用类函数的方式记为:
bool withdraw(account_id, amount)
withdraw的语义是从account_id对应的账户中扣除amount数额的钱;如果扣除成功则返回true,账户余额减少amount;如果扣除失败则返回false,账户余额不变。值得注意的是:和本地环境相比,我们不能轻易假设分布式环境的可靠性。一种典型的情况是withdraw请求已经被服务器端正确处理,但服务器端的返回结果由于网络等原因被掉丢了,导致客户端无法得知处理结果。如果是在网页上,一些不恰当的设计可能会使用户认为上一次操作失败了,然后刷新页面,这就导致了withdraw被调用两次,账户也被多扣了一次钱。如图1所示:
图1
这个问题的解决方案一是采用分布式事务,通过引入支持分布式事务的中间件来保证withdraw功能的事务性。分布式事务的优点是对于调用者很简单,复杂性都交给了中间件来管理。缺点则是一方面架构太重量级,容易被绑在特定的中间件上,不利于异构系统的集成;另一方面分布式事务虽然能保证事务的ACID性质,而但却无法提供性能和可用性的保证。
另一种更轻量级的解决方案是幂等设计。我们可以通过一些技巧把withdraw变成幂等的,比如:
int create_ticket()
bool idempotent_withdraw(ticket_id, account_id, amount)
create_ticket的语义是获取一个服务器端生成的唯一的处理号ticket_id,它将用于标识后续的操作。idempotent_withdraw和withdraw的区别在于关联了一个ticket_id,一个ticket_id表示的操作至多只会被处理一次,每次调用都将返回第一次调用时的处理结果。这样,idempotent_withdraw就符合幂等性了,客户端就可以放心地多次调用。
基于幂等性的解决方案中一个完整的取钱流程被分解成了两个步骤:1.调用create_ticket()获取ticket_id;2.调用idempotent_withdraw(ticket_id, account_id, amount)。虽然create_ticket不是幂等的,但在这种设计下,它对系统状态的影响可以忽略,加上idempotent_withdraw是幂等的,所以任何一步由于网络等原因失败或超时,客户端都可以重试,直到获得结果。如图2所示:
图2
和分布式事务相比,幂等设计的优势在于它的轻量级,容易适应异构环境,以及性能和可用性方面。在某些性能要求比较高的应用,幂等设计往往是唯一的选择。

HTTP的幂等性

HTTP协议本身是一种面向资源的应用层协议,但对HTTP协议的使用实际上存在着两种不同的方式:一种是RESTful的,它把HTTP当成应用层协议,比较忠实地遵守了HTTP协议的各种规定;另一种是SOA的,它并没有完全把HTTP当成应用层协议,而是把HTTP协议作为了传输层协议,然后在HTTP之上建立了自己的应用层协议。本文所讨论的HTTP幂等性主要针对RESTful风格的,不过正如上一节所看到的那样,幂等性并不属于特定的协议,它是分布式系统的一种特性;所以,不论是SOA还是RESTful的Web API设计都应该考虑幂等性。下面将介绍HTTP GET、DELETE、PUT、POST四种主要方法的语义和幂等性。
HTTP GET方法用于获取资源,不应有副作用,所以是幂等的。比如:GET http://www.bank.com/account/123456,不会改变资源的状态,不论调用一次还是N次都没有副作用。请注意,这里强调的是一次和N次具有相同的副作用,而不是每次GET的结果相同。GET http://www.news.com/latest-news这个HTTP请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足幂等性的。
HTTP DELETE方法用于删除资源,有副作用,但它应该满足幂等性。比如:DELETE http://www.forum.com/article/4231,调用一次和N次对系统产生的副作用是相同的,即删掉id为4231的帖子;因此,调用者可以多次调用或刷新页面而不必担心引起错误。
比较容易混淆的是HTTP POST和PUT。POST和PUT的区别容易被简单地误认为“POST表示创建资源,PUT表示更新资源”;而实际上,二者均可用于创建资源,更为本质的差别是在幂等性方面。在HTTP规范中对POST和PUT是这样定义的:
The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line ...... If a resource has been created on the origin server, the response SHOULD be 201 (Created) and contain an entity which describes the status of the request and refers to the new resource, and a Location header.

The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one residing on the origin server. If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI.
POST所对应的URI并非创建的资源本身,而是资源的接收者。比如:POST http://www.forum.com/articles的语义是在http://www.forum.com/articles下创建一篇帖子,HTTP响应中应包含帖子的创建状态以及帖子的URI。两次相同的POST请求会在服务器端创建两份资源,它们具有不同的URI;所以,POST方法不具备幂等性。而PUT所对应的URI是要创建或更新的资源本身。比如:PUT http://www.forum/articles/4231的语义是创建或更新ID为4231的帖子。对同一URI进行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有幂等性。
在介绍了几种操作的语义和幂等性之后,我们来看看如何通过Web API的形式实现前面所提到的取款功能。很简单,用POST /tickets来实现create_ticket;用PUT /accounts/account_id/ticket_id&amount=xxx来实现idempotent_withdraw。值得注意的是严格来讲amount参数不应该作为URI的一部分,真正的URI应该是/accounts/account_id/ticket_id,而amount应该放在请求的body中。这种模式可以应用于很多场合,比如:论坛网站中防止意外的重复发帖。

总结

上面简单介绍了幂等性的概念,用幂等设计取代分布式事务的方法,以及HTTP主要方法的语义和幂等性特征。其实,如果要追根溯源,幂等性是数学中的一个概念,表达的是N次变换与1次变换的结果相同,有兴趣的读者可以从Wikipedia上进一步了解。

参考

RFC 2616, Hypertext Transfer Protocol -- HTTP/1.1, Method Definitions
The Importance of Idempotence
Stackoverflow - PUT vs POST in REST
原文地址:https://www.cnblogs.com/weidagang2046/archive/2011/06/04/idempotence.html

LockSupport

AQS使用LockSupport这个工具类来完成阻塞或者唤醒一个线程的工作 
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在进程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。但是注意许可不可重入,也就是说只能调用一次park()方法,否则会一直阻塞。
LockSupport定义了一系列以park开头的方法来阻塞当前线程,unpark(Thread thread)方法来唤醒一个被阻塞的线程。如下:

park(Object blocker)方法的blocker参数,主要是用来标识当前线程在等待的对象,该对象主要用于问题排查和系统监控。
park方法和unpark(Thread thread)都是成对出现的,同时unpark必须要在park执行之后执行,当然并不是说没有不调用unpark线程就会一直阻塞,park有一个方法,它带了时间戳(parkNanos(long nanos):为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用)。
park()方法的源码如下:

/**
 * Basic thread blocking primitives for creating locks and other
 * synchronization classes.
 *
 * <p>This class associates, with each thread that uses it, a permit
 * (in the sense of the {@link java.util.concurrent.Semaphore
 * Semaphore} class). A call to {@code park} will return immediately
 * if the permit is available, consuming it in the process; otherwise
 * it <em>may</em> block.  A call to {@code unpark} makes the permit
 * available, if it was not already available. (Unlike with Semaphores
 * though, permits do not accumulate. There is at most one.)
 *
 * <p>Methods {@code park} and {@code unpark} provide efficient
 * means of blocking and unblocking threads that do not encounter the
 * problems that cause the deprecated methods {@code Thread.suspend}
 * and {@code Thread.resume} to be unusable for such purposes: Races
 * between one thread invoking {@code park} and another thread trying
 * to {@code unpark} it will preserve liveness, due to the
 * permit. Additionally, {@code park} will return if the caller's
 * thread was interrupted, and timeout versions are supported. The
 * {@code park} method may also return at any other time, for "no
 * reason", so in general must be invoked within a loop that rechecks
 * conditions upon return. In this sense {@code park} serves as an
 * optimization of a "busy wait" that does not waste as much time
 * spinning, but must be paired with an {@code unpark} to be
 * effective.
 *
 * <p>The three forms of {@code park} each also support a
 * {@code blocker} object parameter. This object is recorded while
 * the thread is blocked to permit monitoring and diagnostic tools to
 * identify the reasons that threads are blocked. (Such tools may
 * access blockers using method {@link #getBlocker(Thread)}.)
 * The use of these forms rather than the original forms without this
 * parameter is strongly encouraged. The normal argument to supply as
 * a {@code blocker} within a lock implementation is {@code this}.
 *
 * <p>These methods are designed to be used as tools for creating
 * higher-level synchronization utilities, and are not in themselves
 * useful for most concurrency control applications.  The {@code park}
 * method is designed for use only in constructions of the form:
 *
 *  <pre> {@code
 * while (!canProceed()) { ... LockSupport.park(this); }}</pre>
 *
 * where neither {@code canProceed} nor any other actions prior to the
 * call to {@code park} entail locking or blocking.  Because only one
 * permit is associated with each thread, any intermediary uses of
 * {@code park} could interfere with its intended effects.
 *
 * <p><b>Sample Usage.</b> Here is a sketch of a first-in-first-out
 * non-reentrant lock class:
 *  <pre> {@code
 * class FIFOMutex {
 *   private final AtomicBoolean locked = new AtomicBoolean(false);
 *   private final Queue<Thread> waiters
 *     = new ConcurrentLinkedQueue<Thread>();
 *
 *   public void lock() {
 *     boolean wasInterrupted = false;
 *     Thread current = Thread.currentThread();
 *     waiters.add(current);
 *
 *     // Block while not first in queue or cannot acquire lock
 *     while (waiters.peek() != current ||
 *            !locked.compareAndSet(false, true)) {
 *       LockSupport.park(this);
 *       if (Thread.interrupted()) // ignore interrupts while waiting
 *         wasInterrupted = true;
 *     }
 *
 *     waiters.remove();
 *     if (wasInterrupted)          // reassert interrupt status on exit
 *       current.interrupt();
 *   }
 *
 *   public void unlock() {
 *     locked.set(false);
 *     LockSupport.unpark(waiters.peek());
 *   }
 * }}</pre>
 */
public class LockSupport {
    private LockSupport() {} // Cannot be instantiated.
    private static void setBlocker(Thread t, Object arg) {
        // Even though volatile, hotspot doesn't need a write barrier here.
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }
    /**
     * Makes available the permit for the given thread, if it
     * was not already available.  If the thread was blocked on
     * {@code park} then it will unblock.  Otherwise, its next call
     * to {@code park} is guaranteed not to block. This operation
     * is not guaranteed to have any effect at all if the given
     * thread has not been started.
     *
     * @param thread the thread to unpark, or {@code null}, in which case
     *        this operation has no effect
     */
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
    /**
     * Disables the current thread for thread scheduling purposes unless the
     * permit is available.
     *
     * <p>If the permit is available then it is consumed and the call returns
     * immediately; otherwise
     * the current thread becomes disabled for thread scheduling
     * purposes and lies dormant until one of three things happens:
     *
     * <ul>
     * <li>Some other thread invokes {@link #unpark unpark} with the
     * current thread as the target; or
     *
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     *
     * <li>The call spuriously (that is, for no reason) returns.
     * </ul>
     *
     * <p>This method does <em>not</em> report which of these caused the
     * method to return. Callers should re-check the conditions which caused
     * the thread to park in the first place. Callers may also determine,
     * for example, the interrupt status of the thread upon return.
     *
     * @param blocker the synchronization object responsible for this
     *        thread parking
     * @since 1.6
     */
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    /**
     * Disables the current thread for thread scheduling purposes, for up to
     * the specified waiting time, unless the permit is available.
     *
     * <p>If the permit is available then it is consumed and the call
     * returns immediately; otherwise the current thread becomes disabled
     * for thread scheduling purposes and lies dormant until one of four
     * things happens:
     *
     * <ul>
     * <li>Some other thread invokes {@link #unpark unpark} with the
     * current thread as the target; or
     *
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     *
     * <li>The specified waiting time elapses; or
     *
     * <li>The call spuriously (that is, for no reason) returns.
     * </ul>
     *
     * <p>This method does <em>not</em> report which of these caused the
     * method to return. Callers should re-check the conditions which caused
     * the thread to park in the first place. Callers may also determine,
     * for example, the interrupt status of the thread, or the elapsed time
     * upon return.
     *
     * @param blocker the synchronization object responsible for this
     *        thread parking
     * @param nanos the maximum number of nanoseconds to wait
     * @since 1.6
     */
    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            UNSAFE.park(false, nanos);
            setBlocker(t, null);
        }
    }
    /**
     * Disables the current thread for thread scheduling purposes, until
     * the specified deadline, unless the permit is available.
     *
     * <p>If the permit is available then it is consumed and the call
     * returns immediately; otherwise the current thread becomes disabled
     * for thread scheduling purposes and lies dormant until one of four
     * things happens:
     *
     * <ul>
     * <li>Some other thread invokes {@link #unpark unpark} with the
     * current thread as the target; or
     *
     * <li>Some other thread {@linkplain Thread#interrupt interrupts} the
     * current thread; or
     *
     * <li>The specified deadline passes; or
     *
     * <li>The call spuriously (that is, for no reason) returns.
     * </ul>
     *
     * <p>This method does <em>not</em> report which of these caused the
     * method to return. Callers should re-check the conditions which caused
     * the thread to park in the first place. Callers may also determine,
     * for example, the interrupt status of the thread, or the current time
     * upon return.
     *
     * @param blocker the synchronization object responsible for this
     *        thread parking
     * @param deadline the absolute time, in milliseconds from the Epoch,
     *        to wait until
     * @since 1.6
     */
    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }
    /**
     * Returns the blocker object supplied to the most recent
     * invocation of a park method that has not yet unblocked, or null
     * if not blocked.  The value returned is just a momentary
     * snapshot -- the thread may have since unblocked or blocked on a
     * different blocker object.
     *
     * @param t the thread
     * @return the blocker
     * @throws NullPointerException if argument is null
     * @since 1.6
     */
    public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
    }
    /**
     * Disables the current thread for thread scheduling purposes unless the
     * permit is available.
     *
     * <p>If the permit is available then it is consumed and the call
     * returns immediately; otherwise the current thread becomes disabled
     * for thread scheduling purposes and lies dormant until one of three
     * things happens:
     *
     * <ul>
     *
     * <li>Some other thread invokes {@link #unpark unpark} with the
     * current thread as the target; or
     *
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     *
     * <li>The call spuriously (that is, for no reason) returns.
     * </ul>
     *
     * <p>This method does <em>not</em> report which of these caused the
     * method to return. Callers should re-check the conditions which caused
     * the thread to park in the first place. Callers may also determine,
     * for example, the interrupt status of the thread upon return.
     */
    public static void park() {
        UNSAFE.park(false, 0L);
    }
    /**
     * Disables the current thread for thread scheduling purposes, for up to
     * the specified waiting time, unless the permit is available.
     *
     * <p>If the permit is available then it is consumed and the call
     * returns immediately; otherwise the current thread becomes disabled
     * for thread scheduling purposes and lies dormant until one of four
     * things happens:
     *
     * <ul>
     * <li>Some other thread invokes {@link #unpark unpark} with the
     * current thread as the target; or
     *
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     *
     * <li>The specified waiting time elapses; or
     *
     * <li>The call spuriously (that is, for no reason) returns.
     * </ul>
     *
     * <p>This method does <em>not</em> report which of these caused the
     * method to return. Callers should re-check the conditions which caused
     * the thread to park in the first place. Callers may also determine,
     * for example, the interrupt status of the thread, or the elapsed time
     * upon return.
     *
     * @param nanos the maximum number of nanoseconds to wait
     */
    public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }
    /**
     * Disables the current thread for thread scheduling purposes, until
     * the specified deadline, unless the permit is available.
     *
     * <p>If the permit is available then it is consumed and the call
     * returns immediately; otherwise the current thread becomes disabled
     * for thread scheduling purposes and lies dormant until one of four
     * things happens:
     *
     * <ul>
     * <li>Some other thread invokes {@link #unpark unpark} with the
     * current thread as the target; or
     *
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     *
     * <li>The specified deadline passes; or
     *
     * <li>The call spuriously (that is, for no reason) returns.
     * </ul>
     *
     * <p>This method does <em>not</em> report which of these caused the
     * method to return. Callers should re-check the conditions which caused
     * the thread to park in the first place. Callers may also determine,
     * for example, the interrupt status of the thread, or the current time
     * upon return.
     *
     * @param deadline the absolute time, in milliseconds from the Epoch,
     *        to wait until
     */
    public static void parkUntil(long deadline) {
        UNSAFE.park(true, deadline);
    }
    /**
     * Returns the pseudo-randomly initialized or updated secondary seed.
     * Copied from ThreadLocalRandom due to package access restrictions.
     */
    static final int nextSecondarySeed() {
        int r;
        Thread t = Thread.currentThread();
        if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {
            r ^= r << 13;   // xorshift
            r ^= r >>> 17;
            r ^= r << 5;
        }
        else if ((r = java.util.concurrent.ThreadLocalRandom.current().nextInt()) == 0)
            r = 1; // avoid zero
        UNSAFE.putInt(t, SECONDARY, r);
        return r;
    }
    // Hotspot implementation via intrinsics API
    private static final sun.misc.Unsafe UNSAFE;
    private static final long parkBlockerOffset;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            parkBlockerOffset = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception ex) { throw new Error(ex); }
    }
}



        
两个都是native本地方法。Unsafe 是一个比较危险的类,主要是用于执行低级别、不安全的方法集合。尽管这个类和所有的方法都是公开的(public),但是这个类的使用仍然受限,你无法在自己的java程序中直接使用该类,因为只有授信的代码才能获得该类的实例。

参考资料

  1. 方腾飞:《Java并发编程的艺术》
  2. LockSupport的park和unpark的基本使用,以及对线程中断的响应性


CountDownLatch的用法
CountDownLatch俗称闭锁,是一个同步工具类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
主要方法
 public CountDownLatch(int count); //构造方法参数指定了计数器的次数
 public void countDown(); //当前线程调用此方法,则计数器减一
 public void await() throws InterruptedException  //调用此方法会一直阻塞当前线程,直到计时器的值为0
 注意:countDown最好在finally中调用
示例如下:
  1. public class CountDownLatchDemo {  
  2.     final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
  3.     public static void main(String[] args) throws InterruptedException {  
  4.         CountDownLatch latch=new CountDownLatch(2);//两个工人的协作  
  5.         Worker worker1=new Worker("zhang san"5000, latch);  
  6.         Worker worker2=new Worker("li si"8000, latch);  
  7.         worker1.start();//  
  8.         worker2.start();//  
  9.         latch.await();//等待所有工人完成工作  
  10.         System.out.println("all work done at "+sdf.format(new Date()));  
  11.     }  
  12.       
  13.       
  14.     static class Worker extends Thread{  
  15.         String workerName;   
  16.         int workTime;  
  17.         CountDownLatch latch;  
  18.         public Worker(String workerName ,int workTime ,CountDownLatch latch){  
  19.              this.workerName=workerName;  
  20.              this.workTime=workTime;  
  21.              this.latch=latch;  
  22.         }  
  23.         public void run(){  
  24.             System.out.println("Worker "+workerName+" do work begin at "+sdf.format(new Date()));  
  25.             doWork();//工作了  
  26.             System.out.println("Worker "+workerName+" do work complete at "+sdf.format(new Date()));  
  27.             latch.countDown();//工人完成工作,计数器减一  
  28.   
  29.         }  
  30.           
  31.         private void doWork(){  
  32.             try {  
  33.                 Thread.sleep(workTime);  
  34.             } catch (InterruptedException e) {  
  35.                 e.printStackTrace();  
  36.             }  
  37.         }  
  38.     }  
  39.       
  40.        
  41. }  
输出:
Worker zhang san do work begin at 2011-04-14 11:05:11
Worker li si do work begin at 2011-04-14 11:05:11
Worker zhang san do work complete at 2011-04-14 11:05:16
Worker li si do work complete at 2011-04-14 11:05:19
all work done at 2011-04-14 11:05:19


CountDownLatch源码解析
内部包括一个AQS的子类Sync,构造方法当中指定的计数器赋值到了state变量中,重写tryAcquireShared方法和tryReleaseShared方法。
await方法是调用sync.acquireSharedInterruptibly(1)
countDown方法是调用sync.releaseShared(1);
源码如下:
public class CountDownLatch {
    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
        Sync(int count) {
            setState(count);
        }
        int getCount() {
            return getState();
        }
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }
    private final Sync sync;
    /**
     * Constructs a {@code CountDownLatch} initialized with the given count.
     *
     * @param count the number of times {@link #countDown} must be invoked
     *        before threads can pass through {@link #await}
     * @throws IllegalArgumentException if {@code count} is negative
     */
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
    /**
     * Causes the current thread to wait until the latch has counted down to
     * zero, unless the thread is {@linkplain Thread#interrupt interrupted}.
     *
     * <p>If the current count is zero then this method returns immediately.
     *
     * <p>If the current count is greater than zero then the current
     * thread becomes disabled for thread scheduling purposes and lies
     * dormant until one of two things happen:
     * <ul>
     * <li>The count reaches zero due to invocations of the
     * {@link #countDown} method; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread.
     * </ul>
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * @throws InterruptedException if the current thread is interrupted
     *         while waiting
     */
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    /**
     * Causes the current thread to wait until the latch has counted down to
     * zero, unless the thread is {@linkplain Thread#interrupt interrupted},
     * or the specified waiting time elapses.
     *
     * <p>If the current count is zero then this method returns immediately
     * with the value {@code true}.
     *
     * <p>If the current count is greater than zero then the current
     * thread becomes disabled for thread scheduling purposes and lies
     * dormant until one of three things happen:
     * <ul>
     * <li>The count reaches zero due to invocations of the
     * {@link #countDown} method; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     * <li>The specified waiting time elapses.
     * </ul>
     *
     * <p>If the count reaches zero then the method returns with the
     * value {@code true}.
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * <p>If the specified waiting time elapses then the value {@code false}
     * is returned.  If the time is less than or equal to zero, the method
     * will not wait at all.
     *
     * @param timeout the maximum time to wait
     * @param unit the time unit of the {@code timeout} argument
     * @return {@code true} if the count reached zero and {@code false}
     *         if the waiting time elapsed before the count reached zero
     * @throws InterruptedException if the current thread is interrupted
     *         while waiting
     */
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
    /**
     * Decrements the count of the latch, releasing all waiting threads if
     * the count reaches zero.
     *
     * <p>If the current count is greater than zero then it is decremented.
     * If the new count is zero then all waiting threads are re-enabled for
     * thread scheduling purposes.
     *
     * <p>If the current count equals zero then nothing happens.
     */
    public void countDown() {
        sync.releaseShared(1);
    }
    /**
     * Returns the current count.
     *
     * <p>This method is typically used for debugging and testing purposes.
     *
     * @return the current count
     */
    public long getCount() {
        return sync.getCount();
    }
    /**
     * Returns a string identifying this latch, as well as its state.
     * The state, in brackets, includes the String {@code "Count ="}
     * followed by the current count.
     *
     * @return a string identifying this latch, as well as its state
     */
    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}


Phaser入门
Phaser功能简述
    在JAVA 1.7引入了一个新的并发API:Phaser,一个可重用的同步barrier。在此前,JAVA已经有CyclicBarrier、CountDownLatch这两种同步barrier,但是Phaser更加灵活,而且侧重于“重用”。
一、简述
    1、注册机制:与其他barrier不同的是,Phaser中的“注册的同步者(parties)”会随时间而变化,Phaser可以通过构造器初始化parties个数,也可以在Phaser运行期间随时加入(register)新的parties,以及在运行期间注销(deregister)parties。运行时可以随时加入、注销parties,只会影响Phaser内部的计数器,它建立任何内部的bookkeeping(账本),因此task不能查询自己是否已经注册了,当然你可以通过实现子类来达成这一设计要求。
Java代码  
  1. //伪代码  
  2. Phaser phaser = new Phaser();  
  3. phaser.register();//parties count: 1  
  4. ....  
  5. phaser.arriveAndDeregister()://count : 0;  
  6. ....  
 
    此外,CyclicBarrier、CountDownLatch需要在初始化的构造函数中指定同步者的个数,且运行时无法再次调整。
Java代码  
  1. CountDownLatch countDownLatch = new CountDownLatch(12);  
  2. //count deregister parties after all  
  3. //parties count is 12 all the times  
  4. //if you want change the number of parties, you should create a new instance.  
  5. CyclicBarrier cyclicBarrier = new CyclicBarrier(12);  
 
    2、同步机制:类似于CyclicBarrier,Phaser也可以awaited多次,它的arrivedAndAwaitAdvance()方法的效果类似于CyclicBarrier的await()。Phaser的每个周期(generation)都有一个phase数字,phase 从0开始,当所有的已注册的parties都到达后(arrive)将会导致此phase数字自增(advance),当达到Integer.MAX_VALUE后继续从0开始。这个phase数字用于表示当前parties所处于的“阶段周期”,它既可以标记和控制parties的wait行为、唤醒等待的时机。
    1)Arrival:Phaser中的arrive()、arriveAndDeregister()方法,这两个方法不会阻塞(block),但是会返回相应的phase数字,当此phase中最后一个party也arrive以后,phase数字将会增加,即phase进入下一个周期,同时触发(onAdvance)那些阻塞在上一phase的线程。这一点类似于CyclicBarrier的barrier到达机制;更灵活的是,我们可以通过重写onAdvance方法来实现更多的触发行为。
 
    2)Waiting:Phaser中的awaitAdvance()方法,需要指定一个phase数字,表示此Thread阻塞直到phase推进到此周期,arriveAndAwaitAdvance()方法阻塞到下一周期开始(或者当前phase结束)。不像CyclicBarrier,即使等待Thread已经interrupted,awaitAdvance方法会继续等待。Phaser提供了Interruptible和Timout的阻塞机制,不过当线程Interrupted或者timout之后将会抛出异常,而不会修改Phaser的内部状态。如果必要的话,你可以在遇到此类异常时,进行相应的恢复操作,通常是在调用forceTermination()方法之后。
    Phaser通常在ForJoinPool中执行tasks,它可以在有task阻塞等待advance时,确保其他tasks的充分并行能力。
 
    3、中断(终止):Phaser可以进入Termination状态,可以通过isTermination()方法判断;当Phaser被终止后,所有的同步方法将会立即返回(解除阻塞),不需要等到advance(即advance也会解除阻塞),且这些阻塞方法将会返回一个负值的phase值(awaitAdvance方法、arriveAndAwaitAdvance方法)。当然,向一个termination状态的Phaser注册party将不会有效;此时onAdvance()方法也将会返回true(默认实现),即所有的parties都会被deregister,即register个数为0。
 
    4、Tiering(分层):Phaser可以“分层”,以tree的方式构建Phaser来降低“竞争”。如果一个Phaser中有大量parties,这会导致严重的同步竞争,所以我们可以将它们分组并共享一个parent Phaser,这样可以提高吞吐能力;Phaser中注册和注销parties都会有Child 和parent Phaser自动管理。当Child Phaser中中注册的parties变为非0时(在构造函数Phaser(Phaser parent,int parties),或者register()方法),Child Phaser将会注册到其Parent上;当Child Phaser中的parties变为0时(比如由arrivedAndDegister()方法),那么此时Child Phaser也将从其parent中注销出去。
 
    5、监控:同步的方法只会被register操作调用,对于当前state的监控方法可以在任何时候调用,比如getRegisteredParties()获取已经注册的parties个数,getPhase()获取当前phase周期数等;因为这些方法并非同步,所以只能反映当时的瞬间状态。
 
二、常用的Barrier比较
    1、CountDownLatch
Java代码  
  1. //创建时,就需要指定参与的parties个数  
  2. int parties = 12;  
  3. CountDownLatch latch = new CountDownLatch(parties);  
  4. //线程池中同步task  
  5. ExecutorService executor = Executors.newFixedThreadPool(parties);  
  6. for(int i = 0; i < parties; i++) {  
  7.     executor.execute(new Runnable() {  
  8.         @Override  
  9.         public void run() {  
  10.             try {  
  11.                 //可以在任务执行开始时执行,表示所有的任务都启动后,主线程的await即可解除  
  12.                 //latch.countDown();  
  13.                 //run  
  14.                 //..  
  15.                 Thread.sleep(3000);  
  16.   
  17.             } catch (Exception e) {  
  18.   
  19.             }  
  20.             finally {  
  21.                 //任务执行完毕后:到达  
  22.                 //表示所有的任务都结束,主线程才能继续  
  23.                 latch.countDown();  
  24.             }  
  25.         }  
  26.     });  
  27. }  
  28. latch.await();//主线程阻塞,直到所有的parties到达  
  29. //latch上所有的parties都达到后,再次执行await将不会有效,  
  30. //即barrier是不可重用的  
  31. executor.shutdown();  
 
    2、CyclicBarrier
Java代码  
  1. //创建时,就需要指定参与的parties个数  
  2. int parties = 12;  
  3. CyclicBarrier barrier = new CyclicBarrier(parties);  
  4. //线程池中同步task  
  5. ExecutorService executor = Executors.newFixedThreadPool(parties);  
  6. for(int i = 0; i < parties; i++) {  
  7.     executor.execute(new Runnable() {  
  8.         @Override  
  9.         public void run() {  
  10.             try {  
  11.                 int i = 0;  
  12.                 while (i < 3 && !barrier.isBroken()) {  
  13.                     System.out.println("generation begin:" + i + ",tid:" + Thread.currentThread().getId());  
  14.                     Thread.sleep(3000);  
  15.                     //如果所有的parties都到达,则开启新的一次周期(generation)  
  16.                     //barrier可以被重用  
  17.                     barrier.await();  
  18.                     i++;  
  19.                 }  
  20.   
  21.             } catch (Exception e) {  
  22.                 e.printStackTrace();  
  23.             }  
  24.             finally {  
  25.   
  26.             }  
  27.         }  
  28.     });  
  29. }  
  30. Thread.sleep(100000);  
 
    3、Phaser
Java代码  
  1. //创建时,就需要指定参与的parties个数  
  2. int parties = 12;  
  3. //可以在创建时不指定parties  
  4. // 而是在运行时,随时注册和注销新的parties  
  5. Phaser phaser = new Phaser();  
  6. //主线程先注册一个  
  7. //对应下文中,主线程可以等待所有的parties到达后再解除阻塞(类似与CountDownLatch)  
  8. phaser.register();  
  9. ExecutorService executor = Executors.newFixedThreadPool(parties);  
  10. for(int i = 0; i < parties; i++) {  
  11.     phaser.register();//每创建一个task,我们就注册一个party  
  12.     executor.execute(new Runnable() {  
  13.         @Override  
  14.         public void run() {  
  15.             try {  
  16.                 int i = 0;  
  17.                 while (i < 3 && !phaser.isTerminated()) {  
  18.                     System.out.println("Generation:" + phaser.getPhase());  
  19.                     Thread.sleep(3000);  
  20.                     //等待同一周期内,其他Task到达  
  21.                     //然后进入新的周期,并继续同步进行  
  22.                     phaser.arriveAndAwaitAdvance();  
  23.                     i++;//我们假定,运行三个周期即可  
  24.                 }  
  25.             } catch (Exception e) {  
  26.   
  27.             }  
  28.             finally {  
  29.                 phaser.arriveAndDeregister();  
  30.             }  
  31.         }  
  32.     });  
  33. }  
  34. //主线程到达,且注销自己  
  35. //此后线程池中的线程即可开始按照周期,同步执行。  
  36. phaser.arriveAndDeregister();  
 
三、API简述
     1、Phaser():构造函数,创建一个Phaser;默认parties个数为0。此后我们可以通过register()、bulkRegister()方法来注册新的parties。每个Phaser实例内部,都持有几个状态数据:termination状态、已经注册的parties个数(registeredParties)、当前phase下已到达的parties个数(arrivedParties)、当前phase周期数,还有2个同步阻塞队列Queue。Queue中保存了所有的waiter,即因为advance而等待的线程信息;这两个Queue分别为evenQ和oddQ,这两个Queue在实现上没有任何区别,Queue的元素为QNode,每个QNode保存一个waiter的信息,比如Thread引用、阻塞的phase、超时的deadline、是否支持interrupted响应等。两个Queue,其中一个保存当前phase中正在使用的waiter,另一个备用,当phase为奇数时使用evenQ、oddQ备用,偶数时相反,即两个Queue轮换使用。当advance事件触发期间,新register的parties将会被放在备用的Queue中,advance只需要响应另一个Queue中的waiters即可,避免出现混乱。
 
    2、Phaser(int parties):构造函数,初始一定数量的parties;相当于直接regsiter此数量的parties。
    3、arrive():到达,阻塞,等到当前phase下其他parties到达。如果没有register(即已register数量为0),调用此方法将会抛出异常,此方法返回当前phase周期数,如果Phaser已经终止,则返回负数。
    4、arriveAndDeregister():到达,并注销一个parties数量,非阻塞方法。注销,将会导致Phaser内部的parties个数减一(只影响当前phase),即下一个phase需要等待arrive的parties数量将减一。异常机制和返回值,与arrive方法一致。
    5、arriveAndAwaitAdvance():到达,且阻塞直到其他parties都到达,且advance。此方法等同于awaitAdvance(arrive())。如果你希望阻塞机制支持timeout、interrupted响应,可以使用类似的其他方法(参见下文)。如果你希望到达后且注销,而且阻塞等到当前phase下其他的parties到达,可以使用awaitAdvance(arriveAndDeregister())方法组合。此方法的异常机制和返回值同arrive()。
    6、awaitAdvance(int phase):阻塞方法,等待phase周期数下其他所有的parties都到达。如果指定的phase与Phaser当前的phase不一致,则立即返回。
    7、awaitAdvanceInterruptibly(int phase):阻塞方法,同awaitAdvance,只是支持interrupted响应,即waiter线程如果被外部中断,则此方法立即返回,并抛出InterrutedException。
    8、awaitAdvanceInterruptibly(int phase,long timeout,TimeUnit unit):阻塞方法,同awaitAdvance,支持timeout类型的interrupted响应,即当前线程阻塞等待约定的时长,超时后以TimeoutException异常方式返回。
    9、forceTermination():强制终止,此后Phaser对象将不可用,即register等将不再有效。此方法将会导致Queue中所有的waiter线程被唤醒。
    10、register():新注册一个party,导致Phaser内部registerPaties数量加1;如果此时onAdvance方法正在执行,此方法将会等待它执行完毕后才会返回。此方法返回当前的phase周期数,如果Phaser已经中断,将会返回负数。
    11、bulkRegister(int parties):批量注册多个parties数组,规则同10、。
    12、getArrivedParties():获取已经到达的parties个数。
    13、getPhase():获取当前phase周期数。如果Phaser已经中断,则返回负值。
    14、getRegisteredParties():获取已经注册的parties个数。
    15、getUnarrivedParties():获取尚未到达的parties个数。
    16、onAdvance(int phase,int registeredParties):这个方法比较特殊,表示当进入下一个phase时可以进行的事件处理,如果返回true表示此Phaser应该终止(此后将会把Phaser的状态为termination,即isTermination()将返回true。),否则可以继续进行。phase参数表示当前周期数,registeredParties表示当前已经注册的parties个数。
    默认实现为:return registeredParties == 0;在很多情况下,开发者可以通过重写此方法,来实现自定义的advance时间处理机制。

Phaser源码解析
-----
/**
 * A reusable synchronization barrier, similar in functionality to
 * {@link java.util.concurrent.CyclicBarrier CyclicBarrier} and
 * {@link java.util.concurrent.CountDownLatch CountDownLatch}
 * but supporting more flexible usage.
 *
 * <p><b>Registration.</b> Unlike the case for other barriers, the
 * number of parties <em>registered</em> to synchronize on a phaser
 * may vary over time.  Tasks may be registered at any time (using
 * methods {@link #register}, {@link #bulkRegister}, or forms of
 * constructors establishing initial numbers of parties), and
 * optionally deregistered upon any arrival (using {@link
 * #arriveAndDeregister}).  As is the case with most basic
 * synchronization constructs, registration and deregistration affect
 * only internal counts; they do not establish any further internal
 * bookkeeping, so tasks cannot query whether they are registered.
 * (However, you can introduce such bookkeeping by subclassing this
 * class.)
 *
 * <p><b>Synchronization.</b> Like a {@code CyclicBarrier}, a {@code
 * Phaser} may be repeatedly awaited.  Method {@link
 * #arriveAndAwaitAdvance} has effect analogous to {@link
 * java.util.concurrent.CyclicBarrier#await CyclicBarrier.await}. Each
 * generation of a phaser has an associated phase number. The phase
 * number starts at zero, and advances when all parties arrive at the
 * phaser, wrapping around to zero after reaching {@code
 * Integer.MAX_VALUE}. The use of phase numbers enables independent
 * control of actions upon arrival at a phaser and upon awaiting
 * others, via two kinds of methods that may be invoked by any
 * registered party:
 *
 * <ul>
 *
 *   <li> <b>Arrival.</b> Methods {@link #arrive} and
 *       {@link #arriveAndDeregister} record arrival.  These methods
 *       do not block, but return an associated <em>arrival phase
 *       number</em>; that is, the phase number of the phaser to which
 *       the arrival applied. When the final party for a given phase
 *       arrives, an optional action is performed and the phase
 *       advances.  These actions are performed by the party
 *       triggering a phase advance, and are arranged by overriding
 *       method {@link #onAdvance(int, int)}, which also controls
 *       termination. Overriding this method is similar to, but more
 *       flexible than, providing a barrier action to a {@code
 *       CyclicBarrier}.
 *
 *   <li> <b>Waiting.</b> Method {@link #awaitAdvance} requires an
 *       argument indicating an arrival phase number, and returns when
 *       the phaser advances to (or is already at) a different phase.
 *       Unlike similar constructions using {@code CyclicBarrier},
 *       method {@code awaitAdvance} continues to wait even if the
 *       waiting thread is interrupted. Interruptible and timeout
 *       versions are also available, but exceptions encountered while
 *       tasks wait interruptibly or with timeout do not change the
 *       state of the phaser. If necessary, you can perform any
 *       associated recovery within handlers of those exceptions,
 *       often after invoking {@code forceTermination}.  Phasers may
 *       also be used by tasks executing in a {@link ForkJoinPool},
 *       which will ensure sufficient parallelism to execute tasks
 *       when others are blocked waiting for a phase to advance.
 *
 * </ul>
 *
 * <p><b>Termination.</b> A phaser may enter a <em>termination</em>
 * state, that may be checked using method {@link #isTerminated}. Upon
 * termination, all synchronization methods immediately return without
 * waiting for advance, as indicated by a negative return value.
 * Similarly, attempts to register upon termination have no effect.
 * Termination is triggered when an invocation of {@code onAdvance}
 * returns {@code true}. The default implementation returns {@code
 * true} if a deregistration has caused the number of registered
 * parties to become zero.  As illustrated below, when phasers control
 * actions with a fixed number of iterations, it is often convenient
 * to override this method to cause termination when the current phase
 * number reaches a threshold. Method {@link #forceTermination} is
 * also available to abruptly release waiting threads and allow them
 * to terminate.
 *
 * <p><b>Tiering.</b> Phasers may be <em>tiered</em> (i.e.,
 * constructed in tree structures) to reduce contention. Phasers with
 * large numbers of parties that would otherwise experience heavy
 * synchronization contention costs may instead be set up so that
 * groups of sub-phasers share a common parent.  This may greatly
 * increase throughput even though it incurs greater per-operation
 * overhead.
 *
 * <p>In a tree of tiered phasers, registration and deregistration of
 * child phasers with their parent are managed automatically.
 * Whenever the number of registered parties of a child phaser becomes
 * non-zero (as established in the {@link #Phaser(Phaser,int)}
 * constructor, {@link #register}, or {@link #bulkRegister}), the
 * child phaser is registered with its parent.  Whenever the number of
 * registered parties becomes zero as the result of an invocation of
 * {@link #arriveAndDeregister}, the child phaser is deregistered
 * from its parent.
 *
 * <p><b>Monitoring.</b> While synchronization methods may be invoked
 * only by registered parties, the current state of a phaser may be
 * monitored by any caller.  At any given moment there are {@link
 * #getRegisteredParties} parties in total, of which {@link
 * #getArrivedParties} have arrived at the current phase ({@link
 * #getPhase}).  When the remaining ({@link #getUnarrivedParties})
 * parties arrive, the phase advances.  The values returned by these
 * methods may reflect transient states and so are not in general
 * useful for synchronization control.  Method {@link #toString}
 * returns snapshots of these state queries in a form convenient for
 * informal monitoring.
 *
 * <p><b>Sample usages:</b>
 *
 * <p>A {@code Phaser} may be used instead of a {@code CountDownLatch}
 * to control a one-shot action serving a variable number of parties.
 * The typical idiom is for the method setting this up to first
 * register, then start the actions, then deregister, as in:
 *
 *  <pre> {@code
 * void runTasks(List<Runnable> tasks) {
 *   final Phaser phaser = new Phaser(1); // "1" to register self
 *   // create and start threads
 *   for (final Runnable task : tasks) {
 *     phaser.register();
 *     new Thread() {
 *       public void run() {
 *         phaser.arriveAndAwaitAdvance(); // await all creation
 *         task.run();
 *       }
 *     }.start();
 *   }
 *
 *   // allow threads to start and deregister self
 *   phaser.arriveAndDeregister();
 * }}</pre>
 *
 * <p>One way to cause a set of threads to repeatedly perform actions
 * for a given number of iterations is to override {@code onAdvance}:
 *
 *  <pre> {@code
 * void startTasks(List<Runnable> tasks, final int iterations) {
 *   final Phaser phaser = new Phaser() {
 *     protected boolean onAdvance(int phase, int registeredParties) {
 *       return phase >= iterations || registeredParties == 0;
 *     }
 *   };
 *   phaser.register();
 *   for (final Runnable task : tasks) {
 *     phaser.register();
 *     new Thread() {
 *       public void run() {
 *         do {
 *           task.run();
 *           phaser.arriveAndAwaitAdvance();
 *         } while (!phaser.isTerminated());
 *       }
 *     }.start();
 *   }
 *   phaser.arriveAndDeregister(); // deregister self, don't wait
 * }}</pre>
 *
 * If the main task must later await termination, it
 * may re-register and then execute a similar loop:
 *  <pre> {@code
 *   // ...
 *   phaser.register();
 *   while (!phaser.isTerminated())
 *     phaser.arriveAndAwaitAdvance();}</pre>
 *
 * <p>Related constructions may be used to await particular phase numbers
 * in contexts where you are sure that the phase will never wrap around
 * {@code Integer.MAX_VALUE}. For example:
 *
 *  <pre> {@code
 * void awaitPhase(Phaser phaser, int phase) {
 *   int p = phaser.register(); // assumes caller not already registered
 *   while (p < phase) {
 *     if (phaser.isTerminated())
 *       // ... deal with unexpected termination
 *     else
 *       p = phaser.arriveAndAwaitAdvance();
 *   }
 *   phaser.arriveAndDeregister();
 * }}</pre>
 *
 *
 * <p>To create a set of {@code n} tasks using a tree of phasers, you
 * could use code of the following form, assuming a Task class with a
 * constructor accepting a {@code Phaser} that it registers with upon
 * construction. After invocation of {@code build(new Task[n], 0, n,
 * new Phaser())}, these tasks could then be started, for example by
 * submitting to a pool:
 *
 *  <pre> {@code
 * void build(Task[] tasks, int lo, int hi, Phaser ph) {
 *   if (hi - lo > TASKS_PER_PHASER) {
 *     for (int i = lo; i < hi; i += TASKS_PER_PHASER) {
 *       int j = Math.min(i + TASKS_PER_PHASER, hi);
 *       build(tasks, i, j, new Phaser(ph));
 *     }
 *   } else {
 *     for (int i = lo; i < hi; ++i)
 *       tasks[i] = new Task(ph);
 *       // assumes new Task(ph) performs ph.register()
 *   }
 * }}</pre>
 *
 * The best value of {@code TASKS_PER_PHASER} depends mainly on
 * expected synchronization rates. A value as low as four may
 * be appropriate for extremely small per-phase task bodies (thus
 * high rates), or up to hundreds for extremely large ones.
 *
 * <p><b>Implementation notes</b>: This implementation restricts the
 * maximum number of parties to 65535. Attempts to register additional
 * parties result in {@code IllegalStateException}. However, you can and
 * should create tiered phasers to accommodate arbitrarily large sets
 * of participants.
 *
 * @since 1.7
 * @author Doug Lea
 */
public class Phaser {
    /*
     * This class implements an extension of X10 "clocks".  Thanks to
     * Vijay Saraswat for the idea, and to Vivek Sarkar for
     * enhancements to extend functionality.
     */
    /**
     * Primary state representation, holding four bit-fields:
     *
     * unarrived  -- the number of parties yet to hit barrier (bits  0-15)
     * parties    -- the number of parties to wait            (bits 16-31)
     * phase      -- the generation of the barrier            (bits 32-62)
     * terminated -- set if barrier is terminated             (bit  63 / sign)
     *
     * Except that a phaser with no registered parties is
     * distinguished by the otherwise illegal state of having zero
     * parties and one unarrived parties (encoded as EMPTY below).
     *
     * To efficiently maintain atomicity, these values are packed into
     * a single (atomic) long. Good performance relies on keeping
     * state decoding and encoding simple, and keeping race windows
     * short.
     *
     * All state updates are performed via CAS except initial
     * registration of a sub-phaser (i.e., one with a non-null
     * parent).  In this (relatively rare) case, we use built-in
     * synchronization to lock while first registering with its
     * parent.
     *
     * The phase of a subphaser is allowed to lag that of its
     * ancestors until it is actually accessed -- see method
     * reconcileState.
     */
    private volatile long state;
    private static final int  MAX_PARTIES     = 0xffff;
    private static final int  MAX_PHASE       = Integer.MAX_VALUE;
    private static final int  PARTIES_SHIFT   = 16;
    private static final int  PHASE_SHIFT     = 32;
    private static final int  UNARRIVED_MASK  = 0xffff;      // to mask ints
    private static final long PARTIES_MASK    = 0xffff0000L; // to mask longs
    private static final long COUNTS_MASK     = 0xffffffffL;
    private static final long TERMINATION_BIT = 1L << 63;
    // some special values
    private static final int  ONE_ARRIVAL     = 1;
    private static final int  ONE_PARTY       = 1 << PARTIES_SHIFT;
    private static final int  ONE_DEREGISTER  = ONE_ARRIVAL|ONE_PARTY;
    private static final int  EMPTY           = 1;
    // The following unpacking methods are usually manually inlined
    private static int unarrivedOf(long s) {
        int counts = (int)s;
        return (counts == EMPTY) ? 0 : (counts & UNARRIVED_MASK);
    }
    private static int partiesOf(long s) {
        return (int)s >>> PARTIES_SHIFT;
    }
    private static int phaseOf(long s) {
        return (int)(s >>> PHASE_SHIFT);
    }
    private static int arrivedOf(long s) {
        int counts = (int)s;
        return (counts == EMPTY) ? 0 :
            (counts >>> PARTIES_SHIFT) - (counts & UNARRIVED_MASK);
    }
    /**
     * The parent of this phaser, or null if none
     */
    private final Phaser parent;
    /**
     * The root of phaser tree. Equals this if not in a tree.
     */
    private final Phaser root;
    /**
     * Heads of Treiber stacks for waiting threads. To eliminate
     * contention when releasing some threads while adding others, we
     * use two of them, alternating across even and odd phases.
     * Subphasers share queues with root to speed up releases.
     */
    private final AtomicReference<QNode> evenQ;
    private final AtomicReference<QNode> oddQ;
    private AtomicReference<QNode> queueFor(int phase) {
        return ((phase & 1) == 0) ? evenQ : oddQ;
    }
    /**
     * Returns message string for bounds exceptions on arrival.
     */
    private String badArrive(long s) {
        return "Attempted arrival of unregistered party for " +
            stateToString(s);
    }
    /**
     * Returns message string for bounds exceptions on registration.
     */
    private String badRegister(long s) {
        return "Attempt to register more than " +
            MAX_PARTIES + " parties for " + stateToString(s);
    }
    /**
     * Main implementation for methods arrive and arriveAndDeregister.
     * Manually tuned to speed up and minimize race windows for the
     * common case of just decrementing unarrived field.
     *
     * @param adjust value to subtract from state;
     *               ONE_ARRIVAL for arrive,
     *               ONE_DEREGISTER for arriveAndDeregister
     */
    private int doArrive(int adjust) {
        final Phaser root = this.root;
        for (;;) {
            long s = (root == this) ? state : reconcileState();
            int phase = (int)(s >>> PHASE_SHIFT);
            if (phase < 0)
                return phase;
            int counts = (int)s;
            int unarrived = (counts == EMPTY) ? 0 : (counts & UNARRIVED_MASK);
            if (unarrived <= 0)
                throw new IllegalStateException(badArrive(s));
            if (UNSAFE.compareAndSwapLong(this, stateOffset, s, s-=adjust)) {
                if (unarrived == 1) {
                    long n = s & PARTIES_MASK // base of next state
                    int nextUnarrived = (int)n >>> PARTIES_SHIFT;
                    if (root == this) {
                        if (onAdvance(phase, nextUnarrived))
                            n |= TERMINATION_BIT;
                        else if (nextUnarrived == 0)
                            n |= EMPTY;
                        else
                            n |= nextUnarrived;
                        int nextPhase = (phase + 1) & MAX_PHASE;
                        n |= (long)nextPhase << PHASE_SHIFT;
                        UNSAFE.compareAndSwapLong(this, stateOffset, s, n);
                        releaseWaiters(phase);
                    }
                    else if (nextUnarrived == 0) { // propagate deregistration
                        phase = parent.doArrive(ONE_DEREGISTER);
                        UNSAFE.compareAndSwapLong(this, stateOffset,
                                                  s, s | EMPTY);
                    }
                    else
                        phase = parent.doArrive(ONE_ARRIVAL);
                }
                return phase;
            }
        }
    }
    /**
     * Implementation of register, bulkRegister
     *
     * @param registrations number to add to both parties and
     * unarrived fields. Must be greater than zero.
     */
    private int doRegister(int registrations) {
        // adjustment to state
        long adjust = ((long)registrations << PARTIES_SHIFT) | registrations;
        final Phaser parent = this.parent;
        int phase;
        for (;;) {
            long s = (parent == null) ? state : reconcileState();
            int counts = (int)s;
            int parties = counts >>> PARTIES_SHIFT;
            int unarrived = counts & UNARRIVED_MASK;
            if (registrations > MAX_PARTIES - parties)
                throw new IllegalStateException(badRegister(s));
            phase = (int)(s >>> PHASE_SHIFT);
            if (phase < 0)
                break;
            if (counts != EMPTY) {                  // not 1st registration
                if (parent == null || reconcileState() == s) {
                    if (unarrived == 0)             // wait out advance
                        root.internalAwaitAdvance(phase, null);
                    else if (UNSAFE.compareAndSwapLong(this, stateOffset,
                                                       s, s + adjust))
                        break;
                }
            }
            else if (parent == null) {              // 1st root registration
                long next = ((long)phase << PHASE_SHIFT) | adjust;
                if (UNSAFE.compareAndSwapLong(this, stateOffset, s, next))
                    break;
            }
            else {
                synchronized (this) {               // 1st sub registration
                    if (state == s) {               // recheck under lock
                        phase = parent.doRegister(1);
                        if (phase < 0)
                            break;
                        // finish registration whenever parent registration
                        // succeeded, even when racing with termination,
                        // since these are part of the same "transaction".
                        while (!UNSAFE.compareAndSwapLong
                               (this, stateOffset, s,
                                ((long)phase << PHASE_SHIFT) | adjust)) {
                            s = state;
                            phase = (int)(root.state >>> PHASE_SHIFT);
                            // assert (int)s == EMPTY;
                        }
                        break;
                    }
                }
            }
        }
        return phase;
    }
    /**
     * Resolves lagged phase propagation from root if necessary.
     * Reconciliation normally occurs when root has advanced but
     * subphasers have not yet done so, in which case they must finish
     * their own advance by setting unarrived to parties (or if
     * parties is zero, resetting to unregistered EMPTY state).
     *
     * @return reconciled state
     */
    private long reconcileState() {
        final Phaser root = this.root;
        long s = state;
        if (root != this) {
            int phase, p;
            // CAS to root phase with current parties, tripping unarrived
            while ((phase = (int)(root.state >>> PHASE_SHIFT)) !=
                   (int)(s >>> PHASE_SHIFT) &&
                   !UNSAFE.compareAndSwapLong
                   (this, stateOffset, s,
                    s = (((long)phase << PHASE_SHIFT) |
                         ((phase < 0) ? (s & COUNTS_MASK) :
                          (((p = (int)s >>> PARTIES_SHIFT) == 0) ? EMPTY :
                           ((s & PARTIES_MASK) | p))))))
                s = state;
        }
        return s;
    }
    /**
     * Creates a new phaser with no initially registered parties, no
     * parent, and initial phase number 0. Any thread using this
     * phaser will need to first register for it.
     */
    public Phaser() {
        this(null, 0);
    }
    /**
     * Creates a new phaser with the given number of registered
     * unarrived parties, no parent, and initial phase number 0.
     *
     * @param parties the number of parties required to advance to the
     * next phase
     * @throws IllegalArgumentException if parties less than zero
     * or greater than the maximum number of parties supported
     */
    public Phaser(int parties) {
        this(null, parties);
    }
    /**
     * Equivalent to {@link #Phaser(Phaser, int) Phaser(parent, 0)}.
     *
     * @param parent the parent phaser
     */
    public Phaser(Phaser parent) {
        this(parent, 0);
    }
    /**
     * Creates a new phaser with the given parent and number of
     * registered unarrived parties.  When the given parent is non-null
     * and the given number of parties is greater than zero, this
     * child phaser is registered with its parent.
     *
     * @param parent the parent phaser
     * @param parties the number of parties required to advance to the
     * next phase
     * @throws IllegalArgumentException if parties less than zero
     * or greater than the maximum number of parties supported
     */
    public Phaser(Phaser parent, int parties) {
        if (parties >>> PARTIES_SHIFT != 0)
            throw new IllegalArgumentException("Illegal number of parties");
        int phase = 0;
        this.parent = parent;
        if (parent != null) {
            final Phaser root = parent.root;
            this.root = root;
            this.evenQ = root.evenQ;
            this.oddQ = root.oddQ;
            if (parties != 0)
                phase = parent.doRegister(1);
        }
        else {
            this.root = this;
            this.evenQ = new AtomicReference<QNode>();
            this.oddQ = new AtomicReference<QNode>();
        }
        this.state = (parties == 0) ? (long)EMPTY :
            ((long)phase << PHASE_SHIFT) |
            ((long)parties << PARTIES_SHIFT) |
            ((long)parties);
    }
    /**
     * Adds a new unarrived party to this phaser.  If an ongoing
     * invocation of {@link #onAdvance} is in progress, this method
     * may await its completion before returning.  If this phaser has
     * a parent, and this phaser previously had no registered parties,
     * this child phaser is also registered with its parent. If
     * this phaser is terminated, the attempt to register has
     * no effect, and a negative value is returned.
     *
     * @return the arrival phase number to which this registration
     * applied.  If this value is negative, then this phaser has
     * terminated, in which case registration has no effect.
     * @throws IllegalStateException if attempting to register more
     * than the maximum supported number of parties
     */
    public int register() {
        return doRegister(1);
    }
    /**
     * Adds the given number of new unarrived parties to this phaser.
     * If an ongoing invocation of {@link #onAdvance} is in progress,
     * this method may await its completion before returning.  If this
     * phaser has a parent, and the given number of parties is greater
     * than zero, and this phaser previously had no registered
     * parties, this child phaser is also registered with its parent.
     * If this phaser is terminated, the attempt to register has no
     * effect, and a negative value is returned.
     *
     * @param parties the number of additional parties required to
     * advance to the next phase
     * @return the arrival phase number to which this registration
     * applied.  If this value is negative, then this phaser has
     * terminated, in which case registration has no effect.
     * @throws IllegalStateException if attempting to register more
     * than the maximum supported number of parties
     * @throws IllegalArgumentException if {@code parties < 0}
     */
    public int bulkRegister(int parties) {
        if (parties < 0)
            throw new IllegalArgumentException();
        if (parties == 0)
            return getPhase();
        return doRegister(parties);
    }
    /**
     * Arrives at this phaser, without waiting for others to arrive.
     *
     * <p>It is a usage error for an unregistered party to invoke this
     * method.  However, this error may result in an {@code
     * IllegalStateException} only upon some subsequent operation on
     * this phaser, if ever.
     *
     * @return the arrival phase number, or a negative value if terminated
     * @throws IllegalStateException if not terminated and the number
     * of unarrived parties would become negative
     */
    public int arrive() {
        return doArrive(ONE_ARRIVAL);
    }
    /**
     * Arrives at this phaser and deregisters from it without waiting
     * for others to arrive. Deregistration reduces the number of
     * parties required to advance in future phases.  If this phaser
     * has a parent, and deregistration causes this phaser to have
     * zero parties, this phaser is also deregistered from its parent.
     *
     * <p>It is a usage error for an unregistered party to invoke this
     * method.  However, this error may result in an {@code
     * IllegalStateException} only upon some subsequent operation on
     * this phaser, if ever.
     *
     * @return the arrival phase number, or a negative value if terminated
     * @throws IllegalStateException if not terminated and the number
     * of registered or unarrived parties would become negative
     */
    public int arriveAndDeregister() {
        return doArrive(ONE_DEREGISTER);
    }
    /**
     * Arrives at this phaser and awaits others. Equivalent in effect
     * to {@code awaitAdvance(arrive())}.  If you need to await with
     * interruption or timeout, you can arrange this with an analogous
     * construction using one of the other forms of the {@code
     * awaitAdvance} method.  If instead you need to deregister upon
     * arrival, use {@code awaitAdvance(arriveAndDeregister())}.
     *
     * <p>It is a usage error for an unregistered party to invoke this
     * method.  However, this error may result in an {@code
     * IllegalStateException} only upon some subsequent operation on
     * this phaser, if ever.
     *
     * @return the arrival phase number, or the (negative)
     * {@linkplain #getPhase() current phase} if terminated
     * @throws IllegalStateException if not terminated and the number
     * of unarrived parties would become negative
     */
    public int arriveAndAwaitAdvance() {
        // Specialization of doArrive+awaitAdvance eliminating some reads/paths
        final Phaser root = this.root;
        for (;;) {
            long s = (root == this) ? state : reconcileState();
            int phase = (int)(s >>> PHASE_SHIFT);
            if (phase < 0)
                return phase;
            int counts = (int)s;
            int unarrived = (counts == EMPTY) ? 0 : (counts & UNARRIVED_MASK);
            if (unarrived <= 0)
                throw new IllegalStateException(badArrive(s));
            if (UNSAFE.compareAndSwapLong(this, stateOffset, s,
                                          s -= ONE_ARRIVAL)) {
                if (unarrived > 1)
                    return root.internalAwaitAdvance(phase, null);
                if (root != this)
                    return parent.arriveAndAwaitAdvance();
                long n = s & PARTIES_MASK // base of next state
                int nextUnarrived = (int)n >>> PARTIES_SHIFT;
                if (onAdvance(phase, nextUnarrived))
                    n |= TERMINATION_BIT;
                else if (nextUnarrived == 0)
                    n |= EMPTY;
                else
                    n |= nextUnarrived;
                int nextPhase = (phase + 1) & MAX_PHASE;
                n |= (long)nextPhase << PHASE_SHIFT;
                if (!UNSAFE.compareAndSwapLong(this, stateOffset, s, n))
                    return (int)(state >>> PHASE_SHIFT); // terminated
                releaseWaiters(phase);
                return nextPhase;
            }
        }
    }
    /**
     * Awaits the phase of this phaser to advance from the given phase
     * value, returning immediately if the current phase is not equal
     * to the given phase value or this phaser is terminated.
     *
     * @param phase an arrival phase number, or negative value if
     * terminated; this argument is normally the value returned by a
     * previous call to {@code arrive} or {@code arriveAndDeregister}.
     * @return the next arrival phase number, or the argument if it is
     * negative, or the (negative) {@linkplain #getPhase() current phase}
     * if terminated
     */
    public int awaitAdvance(int phase) {
        final Phaser root = this.root;
        long s = (root == this) ? state : reconcileState();
        int p = (int)(s >>> PHASE_SHIFT);
        if (phase < 0)
            return phase;
        if (p == phase)
            return root.internalAwaitAdvance(phase, null);
        return p;
    }
    /**
     * Awaits the phase of this phaser to advance from the given phase
     * value, throwing {@code InterruptedException} if interrupted
     * while waiting, or returning immediately if the current phase is
     * not equal to the given phase value or this phaser is
     * terminated.
     *
     * @param phase an arrival phase number, or negative value if
     * terminated; this argument is normally the value returned by a
     * previous call to {@code arrive} or {@code arriveAndDeregister}.
     * @return the next arrival phase number, or the argument if it is
     * negative, or the (negative) {@linkplain #getPhase() current phase}
     * if terminated
     * @throws InterruptedException if thread interrupted while waiting
     */
    public int awaitAdvanceInterruptibly(int phase)
        throws InterruptedException {
        final Phaser root = this.root;
        long s = (root == this) ? state : reconcileState();
        int p = (int)(s >>> PHASE_SHIFT);
        if (phase < 0)
            return phase;
        if (p == phase) {
            QNode node = new QNode(this, phase, true, false, 0L);
            p = root.internalAwaitAdvance(phase, node);
            if (node.wasInterrupted)
                throw new InterruptedException();
        }
        return p;
    }
    /**
     * Awaits the phase of this phaser to advance from the given phase
     * value or the given timeout to elapse, throwing {@code
     * InterruptedException} if interrupted while waiting, or
     * returning immediately if the current phase is not equal to the
     * given phase value or this phaser is terminated.
     *
     * @param phase an arrival phase number, or negative value if
     * terminated; this argument is normally the value returned by a
     * previous call to {@code arrive} or {@code arriveAndDeregister}.
     * @param timeout how long to wait before giving up, in units of
     *        {@code unit}
     * @param unit a {@code TimeUnit} determining how to interpret the
     *        {@code timeout} parameter
     * @return the next arrival phase number, or the argument if it is
     * negative, or the (negative) {@linkplain #getPhase() current phase}
     * if terminated
     * @throws InterruptedException if thread interrupted while waiting
     * @throws TimeoutException if timed out while waiting
     */
    public int awaitAdvanceInterruptibly(int phase,
                                         long timeout, TimeUnit unit)
        throws InterruptedException, TimeoutException {
        long nanos = unit.toNanos(timeout);
        final Phaser root = this.root;
        long s = (root == this) ? state : reconcileState();
        int p = (int)(s >>> PHASE_SHIFT);
        if (phase < 0)
            return phase;
        if (p == phase) {
            QNode node = new QNode(this, phase, true, true, nanos);
            p = root.internalAwaitAdvance(phase, node);
            if (node.wasInterrupted)
                throw new InterruptedException();
            else if (p == phase)
                throw new TimeoutException();
        }
        return p;
    }
    /**
     * Forces this phaser to enter termination state.  Counts of
     * registered parties are unaffected.  If this phaser is a member
     * of a tiered set of phasers, then all of the phasers in the set
     * are terminated.  If this phaser is already terminated, this
     * method has no effect.  This method may be useful for
     * coordinating recovery after one or more tasks encounter
     * unexpected exceptions.
     */
    public void forceTermination() {
        // Only need to change root state
        final Phaser root = this.root;
        long s;
        while ((s = root.state) >= 0) {
            if (UNSAFE.compareAndSwapLong(root, stateOffset,
                                          s, s | TERMINATION_BIT)) {
                // signal all threads
                releaseWaiters(0); // Waiters on evenQ
                releaseWaiters(1); // Waiters on oddQ
                return;
            }
        }
    }
    /**
     * Returns the current phase number. The maximum phase number is
     * {@code Integer.MAX_VALUE}, after which it restarts at
     * zero. Upon termination, the phase number is negative,
     * in which case the prevailing phase prior to termination
     * may be obtained via {@code getPhase() + Integer.MIN_VALUE}.
     *
     * @return the phase number, or a negative value if terminated
     */
    public final int getPhase() {
        return (int)(root.state >>> PHASE_SHIFT);
    }
    /**
     * Returns the number of parties registered at this phaser.
     *
     * @return the number of parties
     */
    public int getRegisteredParties() {
        return partiesOf(state);
    }
    /**
     * Returns the number of registered parties that have arrived at
     * the current phase of this phaser. If this phaser has terminated,
     * the returned value is meaningless and arbitrary.
     *
     * @return the number of arrived parties
     */
    public int getArrivedParties() {
        return arrivedOf(reconcileState());
    }
    /**
     * Returns the number of registered parties that have not yet
     * arrived at the current phase of this phaser. If this phaser has
     * terminated, the returned value is meaningless and arbitrary.
     *
     * @return the number of unarrived parties
     */
    public int getUnarrivedParties() {
        return unarrivedOf(reconcileState());
    }
    /**
     * Returns the parent of this phaser, or {@code null} if none.
     *
     * @return the parent of this phaser, or {@code null} if none
     */
    public Phaser getParent() {
        return parent;
    }
    /**
     * Returns the root ancestor of this phaser, which is the same as
     * this phaser if it has no parent.
     *
     * @return the root ancestor of this phaser
     */
    public Phaser getRoot() {
        return root;
    }
    /**
     * Returns {@code true} if this phaser has been terminated.
     *
     * @return {@code true} if this phaser has been terminated
     */
    public boolean isTerminated() {
        return root.state < 0L;
    }
    /**
     * Overridable method to perform an action upon impending phase
     * advance, and to control termination. This method is invoked
     * upon arrival of the party advancing this phaser (when all other
     * waiting parties are dormant).  If this method returns {@code
     * true}, this phaser will be set to a final termination state
     * upon advance, and subsequent calls to {@link #isTerminated}
     * will return true. Any (unchecked) Exception or Error thrown by
     * an invocation of this method is propagated to the party
     * attempting to advance this phaser, in which case no advance
     * occurs.
     *
     * <p>The arguments to this method provide the state of the phaser
     * prevailing for the current transition.  The effects of invoking
     * arrival, registration, and waiting methods on this phaser from
     * within {@code onAdvance} are unspecified and should not be
     * relied on.
     *
     * <p>If this phaser is a member of a tiered set of phasers, then
     * {@code onAdvance} is invoked only for its root phaser on each
     * advance.
     *
     * <p>To support the most common use cases, the default
     * implementation of this method returns {@code true} when the
     * number of registered parties has become zero as the result of a
     * party invoking {@code arriveAndDeregister}.  You can disable
     * this behavior, thus enabling continuation upon future
     * registrations, by overriding this method to always return
     * {@code false}:
     *
     * <pre> {@code
     * Phaser phaser = new Phaser() {
     *   protected boolean onAdvance(int phase, int parties) { return false; }
     * }}</pre>
     *
     * @param phase the current phase number on entry to this method,
     * before this phaser is advanced
     * @param registeredParties the current number of registered parties
     * @return {@code true} if this phaser should terminate
     */
    protected boolean onAdvance(int phase, int registeredParties) {
        return registeredParties == 0;
    }
    /**
     * Returns a string identifying this phaser, as well as its
     * state.  The state, in brackets, includes the String {@code
     * "phase = "} followed by the phase number, {@code "parties = "}
     * followed by the number of registered parties, and {@code
     * "arrived = "} followed by the number of arrived parties.
     *
     * @return a string identifying this phaser, as well as its state
     */
    public String toString() {
        return stateToString(reconcileState());
    }
    /**
     * Implementation of toString and string-based error messages
     */
    private String stateToString(long s) {
        return super.toString() +
            "[phase = " + phaseOf(s) +
            " parties = " + partiesOf(s) +
            " arrived = " + arrivedOf(s) + "]";
    }
    // Waiting mechanics
    /**
     * Removes and signals threads from queue for phase.
     */
    private void releaseWaiters(int phase) {
        QNode q;   // first element of queue
        Thread t // its thread
        AtomicReference<QNode> head = (phase & 1) == 0 ? evenQ : oddQ;
        while ((q = head.get()) != null &&
               q.phase != (int)(root.state >>> PHASE_SHIFT)) {
            if (head.compareAndSet(q, q.next) &&
                (t = q.thread) != null) {
                q.thread = null;
                LockSupport.unpark(t);
            }
        }
    }
    /**
     * Variant of releaseWaiters that additionally tries to remove any
     * nodes no longer waiting for advance due to timeout or
     * interrupt. Currently, nodes are removed only if they are at
     * head of queue, which suffices to reduce memory footprint in
     * most usages.
     *
     * @return current phase on exit
     */
    private int abortWait(int phase) {
        AtomicReference<QNode> head = (phase & 1) == 0 ? evenQ : oddQ;
        for (;;) {
            Thread t;
            QNode q = head.get();
            int p = (int)(root.state >>> PHASE_SHIFT);
            if (q == null || ((t = q.thread) != null && q.phase == p))
                return p;
            if (head.compareAndSet(q, q.next) && t != null) {
                q.thread = null;
                LockSupport.unpark(t);
            }
        }
    }
    /** The number of CPUs, for spin control */
    private static final int NCPU = Runtime.getRuntime().availableProcessors();
    /**
     * The number of times to spin before blocking while waiting for
     * advance, per arrival while waiting. On multiprocessors, fully
     * blocking and waking up a large number of threads all at once is
     * usually a very slow process, so we use rechargeable spins to
     * avoid it when threads regularly arrive: When a thread in
     * internalAwaitAdvance notices another arrival before blocking,
     * and there appear to be enough CPUs available, it spins
     * SPINS_PER_ARRIVAL more times before blocking. The value trades
     * off good-citizenship vs big unnecessary slowdowns.
     */
    static final int SPINS_PER_ARRIVAL = (NCPU < 2) ? 1 : 1 << 8;
    /**
     * Possibly blocks and waits for phase to advance unless aborted.
     * Call only on root phaser.
     *
     * @param phase current phase
     * @param node if non-null, the wait node to track interrupt and timeout;
     * if null, denotes noninterruptible wait
     * @return current phase
     */
    private int internalAwaitAdvance(int phase, QNode node) {
        // assert root == this;
        releaseWaiters(phase-1);          // ensure old queue clean
        boolean queued = false;           // true when node is enqueued
        int lastUnarrived = 0;            // to increase spins upon change
        int spins = SPINS_PER_ARRIVAL;
        long s;
        int p;
        while ((p = (int)((s = state) >>> PHASE_SHIFT)) == phase) {
            if (node == null) {           // spinning in noninterruptible mode
                int unarrived = (int)s & UNARRIVED_MASK;
                if (unarrived != lastUnarrived &&
                    (lastUnarrived = unarrived) < NCPU)
                    spins += SPINS_PER_ARRIVAL;
                boolean interrupted = Thread.interrupted();
                if (interrupted || --spins < 0) { // need node to record intr
                    node = new QNode(this, phase, false, false, 0L);
                    node.wasInterrupted = interrupted;
                }
            }
            else if (node.isReleasable()) // done or aborted
                break;
            else if (!queued) {           // push onto queue
                AtomicReference<QNode> head = (phase & 1) == 0 ? evenQ : oddQ;
                QNode q = node.next = head.get();
                if ((q == null || q.phase == phase) &&
                    (int)(state >>> PHASE_SHIFT) == phase) // avoid stale enq
                    queued = head.compareAndSet(q, node);
            }
            else {
                try {
                    ForkJoinPool.managedBlock(node);
                } catch (InterruptedException ie) {
                    node.wasInterrupted = true;
                }
            }
        }
        if (node != null) {
            if (node.thread != null)
                node.thread = null;       // avoid need for unpark()
            if (node.wasInterrupted && !node.interruptible)
                Thread.currentThread().interrupt();
            if (p == phase && (p = (int)(state >>> PHASE_SHIFT)) == phase)
                return abortWait(phase); // possibly clean up on abort
        }
        releaseWaiters(phase);
        return p;
    }
    /**
     * Wait nodes for Treiber stack representing wait queue
     */
    static final class QNode implements ForkJoinPool.ManagedBlocker {
        final Phaser phaser;
        final int phase;
        final boolean interruptible;
        final boolean timed;
        boolean wasInterrupted;
        long nanos;
        final long deadline;
        volatile Thread thread; // nulled to cancel wait
        QNode next;
        QNode(Phaser phaser, int phase, boolean interruptible,
              boolean timed, long nanos) {
            this.phaser = phaser;
            this.phase = phase;
            this.interruptible = interruptible;
            this.nanos = nanos;
            this.timed = timed;
            this.deadline = timed ? System.nanoTime() + nanos : 0L;
            thread = Thread.currentThread();
        }
        public boolean isReleasable() {
            if (thread == null)
                return true;
            if (phaser.getPhase() != phase) {
                thread = null;
                return true;
            }
            if (Thread.interrupted())
                wasInterrupted = true;
            if (wasInterrupted && interruptible) {
                thread = null;
                return true;
            }
            if (timed) {
                if (nanos > 0L) {
                    nanos = deadline - System.nanoTime();
                }
                if (nanos <= 0L) {
                    thread = null;
                    return true;
                }
            }
            return false;
        }
        public boolean block() {
            if (isReleasable())
                return true;
            else if (!timed)
                LockSupport.park(this);
            else if (nanos > 0L)
                LockSupport.parkNanos(this, nanos);
            return isReleasable();
        }
    }
    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long stateOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = Phaser.class;
            stateOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("state"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}


Exchanger的用法
字如其名,Exchanger用于在两个线程当中安全的交换对象
java.util.concurrent

Class Exchanger<V>

java.lang.Objectjava.util.concurrent.Exchanger<V>
Exchanger源码解析
--------
源码如下:
/**
 * A synchronization point at which threads can pair and swap elements
 * within pairs.  Each thread presents some object on entry to the
 * {@link #exchange exchange} method, matches with a partner thread,
 * and receives its partner's object on return.  An Exchanger may be
 * viewed as a bidirectional form of a {@link SynchronousQueue}.
 * Exchangers may be useful in applications such as genetic algorithms
 * and pipeline designs.
 *
 * <p><b>Sample Usage:</b>
 * Here are the highlights of a class that uses an {@code Exchanger}
 * to swap buffers between threads so that the thread filling the
 * buffer gets a freshly emptied one when it needs it, handing off the
 * filled one to the thread emptying the buffer.
 *  <pre> {@code
 * class FillAndEmpty {
 *   Exchanger<DataBuffer> exchanger = new Exchanger<DataBuffer>();
 *   DataBuffer initialEmptyBuffer = ... a made-up type
 *   DataBuffer initialFullBuffer = ...
 *
 *   class FillingLoop implements Runnable {
 *     public void run() {
 *       DataBuffer currentBuffer = initialEmptyBuffer;
 *       try {
 *         while (currentBuffer != null) {
 *           addToBuffer(currentBuffer);
 *           if (currentBuffer.isFull())
 *             currentBuffer = exchanger.exchange(currentBuffer);
 *         }
 *       } catch (InterruptedException ex) { ... handle ... }
 *     }
 *   }
 *
 *   class EmptyingLoop implements Runnable {
 *     public void run() {
 *       DataBuffer currentBuffer = initialFullBuffer;
 *       try {
 *         while (currentBuffer != null) {
 *           takeFromBuffer(currentBuffer);
 *           if (currentBuffer.isEmpty())
 *             currentBuffer = exchanger.exchange(currentBuffer);
 *         }
 *       } catch (InterruptedException ex) { ... handle ...}
 *     }
 *   }
 *
 *   void start() {
 *     new Thread(new FillingLoop()).start();
 *     new Thread(new EmptyingLoop()).start();
 *   }
 * }}</pre>
 *
 * <p>Memory consistency effects: For each pair of threads that
 * successfully exchange objects via an {@code Exchanger}, actions
 * prior to the {@code exchange()} in each thread
 * <a href="package-summary.html#MemoryVisibility"><i>happen-before</i></a>
 * those subsequent to a return from the corresponding {@code exchange()}
 * in the other thread.
 *
 * @since 1.5
 * @author Doug Lea and Bill Scherer and Michael Scott
 * @param <V> The type of objects that may be exchanged
 */
public class Exchanger<V> {
    /*
     * Overview: The core algorithm is, for an exchange "slot",
     * and a participant (caller) with an item:
     *
     * for (;;) {
     *   if (slot is empty) {                       // offer
     *     place item in a Node;
     *     if (can CAS slot from empty to node) {
     *       wait for release;
     *       return matching item in node;
     *     }
     *   }
     *   else if (can CAS slot from node to empty) { // release
     *     get the item in node;
     *     set matching item in node;
     *     release waiting thread;
     *   }
     *   // else retry on CAS failure
     * }
     *
     * This is among the simplest forms of a "dual data structure" --
     * see Scott and Scherer's DISC 04 paper and
     * http://www.cs.rochester.edu/research/synchronization/pseudocode/duals.html
     *
     * This works great in principle. But in practice, like many
     * algorithms centered on atomic updates to a single location, it
     * scales horribly when there are more than a few participants
     * using the same Exchanger. So the implementation instead uses a
     * form of elimination arena, that spreads out this contention by
     * arranging that some threads typically use different slots,
     * while still ensuring that eventually, any two parties will be
     * able to exchange items. That is, we cannot completely partition
     * across threads, but instead give threads arena indices that
     * will on average grow under contention and shrink under lack of
     * contention. We approach this by defining the Nodes that we need
     * anyway as ThreadLocals, and include in them per-thread index
     * and related bookkeeping state. (We can safely reuse per-thread
     * nodes rather than creating them fresh each time because slots
     * alternate between pointing to a node vs null, so cannot
     * encounter ABA problems. However, we do need some care in
     * resetting them between uses.)
     *
     * Implementing an effective arena requires allocating a bunch of
     * space, so we only do so upon detecting contention (except on
     * uniprocessors, where they wouldn't help, so aren't used).
     * Otherwise, exchanges use the single-slot slotExchange method.
     * On contention, not only must the slots be in different
     * locations, but the locations must not encounter memory
     * contention due to being on the same cache line (or more
     * generally, the same coherence unit).  Because, as of this
     * writing, there is no way to determine cacheline size, we define
     * a value that is enough for common platforms.  Additionally,
     * extra care elsewhere is taken to avoid other false/unintended
     * sharing and to enhance locality, including adding padding (via
     * sun.misc.Contended) to Nodes, embedding "bound" as an Exchanger
     * field, and reworking some park/unpark mechanics compared to
     * LockSupport versions.
     *
     * The arena starts out with only one used slot. We expand the
     * effective arena size by tracking collisions; i.e., failed CASes
     * while trying to exchange. By nature of the above algorithm, the
     * only kinds of collision that reliably indicate contention are
     * when two attempted releases collide -- one of two attempted
     * offers can legitimately fail to CAS without indicating
     * contention by more than one other thread. (Note: it is possible
     * but not worthwhile to more precisely detect contention by
     * reading slot values after CAS failures.)  When a thread has
     * collided at each slot within the current arena bound, it tries
     * to expand the arena size by one. We track collisions within
     * bounds by using a version (sequence) number on the "bound"
     * field, and conservatively reset collision counts when a
     * participant notices that bound has been updated (in either
     * direction).
     *
     * The effective arena size is reduced (when there is more than
     * one slot) by giving up on waiting after a while and trying to
     * decrement the arena size on expiration. The value of "a while"
     * is an empirical matter.  We implement by piggybacking on the
     * use of spin->yield->block that is essential for reasonable
     * waiting performance anyway -- in a busy exchanger, offers are
     * usually almost immediately released, in which case context
     * switching on multiprocessors is extremely slow/wasteful.  Arena
     * waits just omit the blocking part, and instead cancel. The spin
     * count is empirically chosen to be a value that avoids blocking
     * 99% of the time under maximum sustained exchange rates on a
     * range of test machines. Spins and yields entail some limited
     * randomness (using a cheap xorshift) to avoid regular patterns
     * that can induce unproductive grow/shrink cycles. (Using a
     * pseudorandom also helps regularize spin cycle duration by
     * making branches unpredictable.)  Also, during an offer, a
     * waiter can "know" that it will be released when its slot has
     * changed, but cannot yet proceed until match is set.  In the
     * mean time it cannot cancel the offer, so instead spins/yields.
     * Note: It is possible to avoid this secondary check by changing
     * the linearization point to be a CAS of the match field (as done
     * in one case in the Scott & Scherer DISC paper), which also
     * increases asynchrony a bit, at the expense of poorer collision
     * detection and inability to always reuse per-thread nodes. So
     * the current scheme is typically a better tradeoff.
     *
     * On collisions, indices traverse the arena cyclically in reverse
     * order, restarting at the maximum index (which will tend to be
     * sparsest) when bounds change. (On expirations, indices instead
     * are halved until reaching 0.) It is possible (and has been
     * tried) to use randomized, prime-value-stepped, or double-hash
     * style traversal instead of simple cyclic traversal to reduce
     * bunching.  But empirically, whatever benefits these may have
     * don't overcome their added overhead: We are managing operations
     * that occur very quickly unless there is sustained contention,
     * so simpler/faster control policies work better than more
     * accurate but slower ones.
     *
     * Because we use expiration for arena size control, we cannot
     * throw TimeoutExceptions in the timed version of the public
     * exchange method until the arena size has shrunken to zero (or
     * the arena isn't enabled). This may delay response to timeout
     * but is still within spec.
     *
     * Essentially all of the implementation is in methods
     * slotExchange and arenaExchange. These have similar overall
     * structure, but differ in too many details to combine. The
     * slotExchange method uses the single Exchanger field "slot"
     * rather than arena array elements. However, it still needs
     * minimal collision detection to trigger arena construction.
     * (The messiest part is making sure interrupt status and
     * InterruptedExceptions come out right during transitions when
     * both methods may be called. This is done by using null return
     * as a sentinel to recheck interrupt status.)
     *
     * As is too common in this sort of code, methods are monolithic
     * because most of the logic relies on reads of fields that are
     * maintained as local variables so can't be nicely factored --
     * mainly, here, bulky spin->yield->block/cancel code), and
     * heavily dependent on intrinsics (Unsafe) to use inlined
     * embedded CAS and related memory access operations (that tend
     * not to be as readily inlined by dynamic compilers when they are
     * hidden behind other methods that would more nicely name and
     * encapsulate the intended effects). This includes the use of
     * putOrderedX to clear fields of the per-thread Nodes between
     * uses. Note that field Node.item is not declared as volatile
     * even though it is read by releasing threads, because they only
     * do so after CAS operations that must precede access, and all
     * uses by the owning thread are otherwise acceptably ordered by
     * other operations. (Because the actual points of atomicity are
     * slot CASes, it would also be legal for the write to Node.match
     * in a release to be weaker than a full volatile write. However,
     * this is not done because it could allow further postponement of
     * the write, delaying progress.)
     */
    /**
     * The byte distance (as a shift value) between any two used slots
     * in the arena.  1 << ASHIFT should be at least cacheline size.
     */
    private static final int ASHIFT = 7;
    /**
     * The maximum supported arena index. The maximum allocatable
     * arena size is MMASK + 1. Must be a power of two minus one, less
     * than (1<<(31-ASHIFT)). The cap of 255 (0xff) more than suffices
     * for the expected scaling limits of the main algorithms.
     */
    private static final int MMASK = 0xff;
    /**
     * Unit for sequence/version bits of bound field. Each successful
     * change to the bound also adds SEQ.
     */
    private static final int SEQ = MMASK + 1;
    /** The number of CPUs, for sizing and spin control */
    private static final int NCPU = Runtime.getRuntime().availableProcessors();
    /**
     * The maximum slot index of the arena: The number of slots that
     * can in principle hold all threads without contention, or at
     * most the maximum indexable value.
     */
    static final int FULL = (NCPU >= (MMASK << 1)) ? MMASK : NCPU >>> 1;
    /**
     * The bound for spins while waiting for a match. The actual
     * number of iterations will on average be about twice this value
     * due to randomization. Note: Spinning is disabled when NCPU==1.
     */
    private static final int SPINS = 1 << 10;
    /**
     * Value representing null arguments/returns from public
     * methods. Needed because the API originally didn't disallow null
     * arguments, which it should have.
     */
    private static final Object NULL_ITEM = new Object();
    /**
     * Sentinel value returned by internal exchange methods upon
     * timeout, to avoid need for separate timed versions of these
     * methods.
     */
    private static final Object TIMED_OUT = new Object();
    /**
     * Nodes hold partially exchanged data, plus other per-thread
     * bookkeeping. Padded via @sun.misc.Contended to reduce memory
     * contention.
     */
    @sun.misc.Contended static final class Node {
        int index;              // Arena index
        int bound;              // Last recorded value of Exchanger.bound
        int collides;           // Number of CAS failures at current bound
        int hash;               // Pseudo-random for spins
        Object item;            // This thread's current item
        volatile Object match // Item provided by releasing thread
        volatile Thread parked; // Set to this thread when parked, else null
    }
    /** The corresponding thread local class */
    static final class Participant extends ThreadLocal<Node> {
        public Node initialValue() { return new Node(); }
    }
    /**
     * Per-thread state
     */
    private final Participant participant;
    /**
     * Elimination array; null until enabled (within slotExchange).
     * Element accesses use emulation of volatile gets and CAS.
     */
    private volatile Node[] arena;
    /**
     * Slot used until contention detected.
     */
    private volatile Node slot;
    /**
     * The index of the largest valid arena position, OR'ed with SEQ
     * number in high bits, incremented on each update.  The initial
     * update from 0 to SEQ is used to ensure that the arena array is
     * constructed only once.
     */
    private volatile int bound;
    /**
     * Exchange function when arenas enabled. See above for explanation.
     *
     * @param item the (non-null) item to exchange
     * @param timed true if the wait is timed
     * @param ns if timed, the maximum wait time, else 0L
     * @return the other thread's item; or null if interrupted; or
     * TIMED_OUT if timed and timed out
     */
    private final Object arenaExchange(Object item, boolean timed, long ns) {
        Node[] a = arena;
        Node p = participant.get();
        for (int i = p.index;;) {                      // access slot at i
            int b, m, c; long j;                       // j is raw array offset
            Node q = (Node)U.getObjectVolatile(a, j = (i << ASHIFT) + ABASE);
            if (q != null && U.compareAndSwapObject(a, j, q, null)) {
                Object v = q.item;                     // release
                q.match = item;
                Thread w = q.parked;
                if (w != null)
                    U.unpark(w);
                return v;
            }
            else if (i <= (m = (b = bound) & MMASK) && q == null) {
                p.item = item;                         // offer
                if (U.compareAndSwapObject(a, j, null, p)) {
                    long end = (timed && m == 0) ? System.nanoTime() + ns : 0L;
                    Thread t = Thread.currentThread(); // wait
                    for (int h = p.hash, spins = SPINS;;) {
                        Object v = p.match;
                        if (v != null) {
                            U.putOrderedObject(p, MATCH, null);
                            p.item = null;             // clear for next use
                            p.hash = h;
                            return v;
                        }
                        else if (spins > 0) {
                            h ^= h << 1; h ^= h >>> 3; h ^= h << 10; // xorshift
                            if (h == 0)                // initialize hash
                                h = SPINS | (int)t.getId();
                            else if (h < 0 &&          // approx 50% true
                                     (--spins & ((SPINS >>> 1) - 1)) == 0)
                                Thread.yield();        // two yields per wait
                        }
                        else if (U.getObjectVolatile(a, j) != p)
                            spins = SPINS;       // releaser hasn't set match yet
                        else if (!t.isInterrupted() && m == 0 &&
                                 (!timed ||
                                  (ns = end - System.nanoTime()) > 0L)) {
                            U.putObject(t, BLOCKER, this); // emulate LockSupport
                            p.parked = t;              // minimize window
                            if (U.getObjectVolatile(a, j) == p)
                                U.park(false, ns);
                            p.parked = null;
                            U.putObject(t, BLOCKER, null);
                        }
                        else if (U.getObjectVolatile(a, j) == p &&
                                 U.compareAndSwapObject(a, j, p, null)) {
                            if (m != 0)                // try to shrink
                                U.compareAndSwapInt(this, BOUND, b, b + SEQ - 1);
                            p.item = null;
                            p.hash = h;
                            i = p.index >>>= 1;        // descend
                            if (Thread.interrupted())
                                return null;
                            if (timed && m == 0 && ns <= 0L)
                                return TIMED_OUT;
                            break;                     // expired; restart
                        }
                    }
                }
                else
                    p.item = null;                     // clear offer
            }
            else {
                if (p.bound != b) {                    // stale; reset
                    p.bound = b;
                    p.collides = 0;
                    i = (i != m || m == 0) ? m : m - 1;
                }
                else if ((c = p.collides) < m || m == FULL ||
                         !U.compareAndSwapInt(this, BOUND, b, b + SEQ + 1)) {
                    p.collides = c + 1;
                    i = (i == 0) ? m : i - 1;          // cyclically traverse
                }
                else
                    i = m + 1;                         // grow
                p.index = i;
            }
        }
    }
    /**
     * Exchange function used until arenas enabled. See above for explanation.
     *
     * @param item the item to exchange
     * @param timed true if the wait is timed
     * @param ns if timed, the maximum wait time, else 0L
     * @return the other thread's item; or null if either the arena
     * was enabled or the thread was interrupted before completion; or
     * TIMED_OUT if timed and timed out
     */
    private final Object slotExchange(Object item, boolean timed, long ns) {
        Node p = participant.get();
        Thread t = Thread.currentThread();
        if (t.isInterrupted()) // preserve interrupt status so caller can recheck
            return null;
        for (Node q;;) {
            if ((q = slot) != null) {
                if (U.compareAndSwapObject(this, SLOT, q, null)) {
                    Object v = q.item;
                    q.match = item;
                    Thread w = q.parked;
                    if (w != null)
                        U.unpark(w);
                    return v;
                }
                // create arena on contention, but continue until slot null
                if (NCPU > 1 && bound == 0 &&
                    U.compareAndSwapInt(this, BOUND, 0, SEQ))
                    arena = new Node[(FULL + 2) << ASHIFT];
            }
            else if (arena != null)
                return null; // caller must reroute to arenaExchange
            else {
                p.item = item;
                if (U.compareAndSwapObject(this, SLOT, null, p))
                    break;
                p.item = null;
            }
        }
        // await release
        int h = p.hash;
        long end = timed ? System.nanoTime() + ns : 0L;
        int spins = (NCPU > 1) ? SPINS : 1;
        Object v;
        while ((v = p.match) == null) {
            if (spins > 0) {
                h ^= h << 1; h ^= h >>> 3; h ^= h << 10;
                if (h == 0)
                    h = SPINS | (int)t.getId();
                else if (h < 0 && (--spins & ((SPINS >>> 1) - 1)) == 0)
                    Thread.yield();
            }
            else if (slot != p)
                spins = SPINS;
            else if (!t.isInterrupted() && arena == null &&
                     (!timed || (ns = end - System.nanoTime()) > 0L)) {
                U.putObject(t, BLOCKER, this);
                p.parked = t;
                if (slot == p)
                    U.park(false, ns);
                p.parked = null;
                U.putObject(t, BLOCKER, null);
            }
            else if (U.compareAndSwapObject(this, SLOT, p, null)) {
                v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
                break;
            }
        }
        U.putOrderedObject(p, MATCH, null);
        p.item = null;
        p.hash = h;
        return v;
    }
    /**
     * Creates a new Exchanger.
     */
    public Exchanger() {
        participant = new Participant();
    }
    /**
     * Waits for another thread to arrive at this exchange point (unless
     * the current thread is {@linkplain Thread#interrupt interrupted}),
     * and then transfers the given object to it, receiving its object
     * in return.
     *
     * <p>If another thread is already waiting at the exchange point then
     * it is resumed for thread scheduling purposes and receives the object
     * passed in by the current thread.  The current thread returns immediately,
     * receiving the object passed to the exchange by that other thread.
     *
     * <p>If no other thread is already waiting at the exchange then the
     * current thread is disabled for thread scheduling purposes and lies
     * dormant until one of two things happens:
     * <ul>
     * <li>Some other thread enters the exchange; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread.
     * </ul>
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting
     * for the exchange,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * @param x the object to exchange
     * @return the object provided by the other thread
     * @throws InterruptedException if the current thread was
     *         interrupted while waiting
     */
    @SuppressWarnings("unchecked")
    public V exchange(V x) throws InterruptedException {
        Object v;
        Object item = (x == null) ? NULL_ITEM : x; // translate null args
        if ((arena != null ||
             (v = slotExchange(item, false, 0L)) == null) &&
            ((Thread.interrupted() || // disambiguates null return
              (v = arenaExchange(item, false, 0L)) == null)))
            throw new InterruptedException();
        return (v == NULL_ITEM) ? null : (V)v;
    }
    /**
     * Waits for another thread to arrive at this exchange point (unless
     * the current thread is {@linkplain Thread#interrupt interrupted} or
     * the specified waiting time elapses), and then transfers the given
     * object to it, receiving its object in return.
     *
     * <p>If another thread is already waiting at the exchange point then
     * it is resumed for thread scheduling purposes and receives the object
     * passed in by the current thread.  The current thread returns immediately,
     * receiving the object passed to the exchange by that other thread.
     *
     * <p>If no other thread is already waiting at the exchange then the
     * current thread is disabled for thread scheduling purposes and lies
     * dormant until one of three things happens:
     * <ul>
     * <li>Some other thread enters the exchange; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     * <li>The specified waiting time elapses.
     * </ul>
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting
     * for the exchange,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * <p>If the specified waiting time elapses then {@link
     * TimeoutException} is thrown.  If the time is less than or equal
     * to zero, the method will not wait at all.
     *
     * @param x the object to exchange
     * @param timeout the maximum time to wait
     * @param unit the time unit of the {@code timeout} argument
     * @return the object provided by the other thread
     * @throws InterruptedException if the current thread was
     *         interrupted while waiting
     * @throws TimeoutException if the specified waiting time elapses
     *         before another thread enters the exchange
     */
    @SuppressWarnings("unchecked")
    public V exchange(V x, long timeout, TimeUnit unit)
        throws InterruptedException, TimeoutException {
        Object v;
        Object item = (x == null) ? NULL_ITEM : x;
        long ns = unit.toNanos(timeout);
        if ((arena != null ||
             (v = slotExchange(item, true, ns)) == null) &&
            ((Thread.interrupted() ||
              (v = arenaExchange(item, true, ns)) == null)))
            throw new InterruptedException();
        if (v == TIMED_OUT)
            throw new TimeoutException();
        return (v == NULL_ITEM) ? null : (V)v;
    }
    // Unsafe mechanics
    private static final sun.misc.Unsafe U;
    private static final long BOUND;
    private static final long SLOT;
    private static final long MATCH;
    private static final long BLOCKER;
    private static final int ABASE;
    static {
        int s;
        try {
            U = sun.misc.Unsafe.getUnsafe();
            Class<?> ek = Exchanger.class;
            Class<?> nk = Node.class;
            Class<?> ak = Node[].class;
            Class<?> tk = Thread.class;
            BOUND = U.objectFieldOffset
                (ek.getDeclaredField("bound"));
            SLOT = U.objectFieldOffset
                (ek.getDeclaredField("slot"));
            MATCH = U.objectFieldOffset
                (nk.getDeclaredField("match"));
            BLOCKER = U.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
            s = U.arrayIndexScale(ak);
            // ABASE absorbs padding in front of element 0
            ABASE = U.arrayBaseOffset(ak) + (1 << ASHIFT);
        } catch (Exception e) {
            throw new Error(e);
        }
        if ((s & (s-1)) != 0 || s > (1 << ASHIFT))
            throw new Error("Unsupported array scale");
    }
}


CyclicBarrier的用法
CyclicBarrier是可重用的屏障,也称栅栏。
1、类说明:
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
2、使用场景:
需要所有的子任务都完成时,才执行主任务,这个时候就可以选择使用CyclicBarrier。
3、常用方法:

await

public int await()
throws InterruptedException,
BrokenBarrierException
在所有参与者都已经在此 barrier 上调用 await方法之前,将一直等待。如果当前线程不是将到达的最后一个线程,出于调度目的,将禁用它,且在发生以下情况之一前,该线程将一直处于休眠状态:
最后一个线程到达;或者
其他某个线程中断当前线程;或者
其他某个线程中断另一个等待线程;或者
其他某个线程在等待 barrier 时超时;或者
其他某个线程在此 barrier 上调用 reset()
如果当前线程:
在进入此方法时已经设置了该线程的中断状态;或者
在等待时被中断
则抛出 InterruptedException,并且清除当前线程的已中断状态。如果在线程处于等待状态时 barrier 被 reset(),或者在调用 await 时 barrier 被损坏,抑或任意一个线程正处于等待状态,则抛出 BrokenBarrierException 异常。
如果任何线程在等待时被 中断,则其他所有等待线程都将抛出 BrokenBarrierException 异常,并将 barrier 置于损坏状态。
如果当前线程是最后一个将要到达的线程,并且构造方法中提供了一个非空的屏障操作,则在允许其他线程继续运行之前,当前线程将运行该操作。如果在执行屏障操作过程中发生异常,则该异常将传播到当前线程中,并将 barrier 置于损坏状态。
 
返回:
到达的当前线程的索引,其中,索引 getParties() - 1 指示将到达的第一个线程,零指示最后一个到达的线程
抛出:
InterruptedException - 如果当前线程在等待时被中断
BrokenBarrierException - 如果另一个 线程在当前线程等待时被中断或超时,或者重置了 barrier,或者在调用 await 时 barrier 被损坏,抑或由于异常而导致屏障操作(如果存在)失败。
4、相关实例
赛跑时,等待所有人都准备好时,才起跑:
[java] view plain copy
  1. public class CyclicBarrierTest {  
  2.   
  3.     public static void main(String[] args) throws IOException, InterruptedException {  
  4.         //如果将参数改为4,但是下面只加入了3个选手,这永远等待下去  
  5.         //Waits until all parties have invoked await on this barrier.   
  6.         CyclicBarrier barrier = new CyclicBarrier(3);  
  7.   
  8.         ExecutorService executor = Executors.newFixedThreadPool(3);  
  9.         executor.submit(new Thread(new Runner(barrier, "1号选手")));  
  10.         executor.submit(new Thread(new Runner(barrier, "2号选手")));  
  11.         executor.submit(new Thread(new Runner(barrier, "3号选手")));  
  12.   
  13.         executor.shutdown();  
  14.     }  
  15. }  
  16.   
  17. class Runner implements Runnable {  
  18.     // 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)  
  19.     private CyclicBarrier barrier;  
  20.   
  21.     private String name;  
  22.   
  23.     public Runner(CyclicBarrier barrier, String name) {  
  24.         super();  
  25.         this.barrier = barrier;  
  26.         this.name = name;  
  27.     }  
  28.   
  29.     @Override  
  30.     public void run() {  
  31.         try {  
  32.             Thread.sleep(1000 * (new Random()).nextInt(8));  
  33.             System.out.println(name + " 准备好了...");  
  34.             // barrier的await方法,在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。  
  35.             barrier.await();  
  36.         } catch (InterruptedException e) {  
  37.             e.printStackTrace();  
  38.         } catch (BrokenBarrierException e) {  
  39.             e.printStackTrace();  
  40.         }  
  41.         System.out.println(name + " 起跑!");  
  42.     }  
  43. }  


输出结果:
3号选手 准备好了...
2号选手 准备好了...
1号选手 准备好了...
1号选手 起跑!
2号选手 起跑!
3号选手 起跑!
转载地址:http://www.itzhai.com/the-introduction-and-use-of-cyclicbarrier.html


CyclicBarrier源码解析
CyclicBarrier基于ReetrantLock和Condtion
源码如下:
/**
 * A synchronization aid that allows a set of threads to all wait for
 * each other to reach a common barrier point.  CyclicBarriers are
 * useful in programs involving a fixed sized party of threads that
 * must occasionally wait for each other. The barrier is called
 * <em>cyclic</em> because it can be re-used after the waiting threads
 * are released.
 *
 * <p>A {@code CyclicBarrier} supports an optional {@link Runnable} command
 * that is run once per barrier point, after the last thread in the party
 * arrives, but before any threads are released.
 * This <em>barrier action</em> is useful
 * for updating shared-state before any of the parties continue.
 *
 * <p><b>Sample usage:</b> Here is an example of using a barrier in a
 * parallel decomposition design:
 *
 *  <pre> {@code
 * class Solver {
 *   final int N;
 *   final float[][] data;
 *   final CyclicBarrier barrier;
 *
 *   class Worker implements Runnable {
 *     int myRow;
 *     Worker(int row) { myRow = row; }
 *     public void run() {
 *       while (!done()) {
 *         processRow(myRow);
 *
 *         try {
 *           barrier.await();
 *         } catch (InterruptedException ex) {
 *           return;
 *         } catch (BrokenBarrierException ex) {
 *           return;
 *         }
 *       }
 *     }
 *   }
 *
 *   public Solver(float[][] matrix) {
 *     data = matrix;
 *     N = matrix.length;
 *     Runnable barrierAction =
 *       new Runnable() { public void run() { mergeRows(...); }};
 *     barrier = new CyclicBarrier(N, barrierAction);
 *
 *     List<Thread> threads = new ArrayList<Thread>(N);
 *     for (int i = 0; i < N; i++) {
 *       Thread thread = new Thread(new Worker(i));
 *       threads.add(thread);
 *       thread.start();
 *     }
 *
 *     // wait until done
 *     for (Thread thread : threads)
 *       thread.join();
 *   }
 * }}</pre>
 *
 * Here, each worker thread processes a row of the matrix then waits at the
 * barrier until all rows have been processed. When all rows are processed
 * the supplied {@link Runnable} barrier action is executed and merges the
 * rows. If the merger
 * determines that a solution has been found then {@code done()} will return
 * {@code true} and each worker will terminate.
 *
 * <p>If the barrier action does not rely on the parties being suspended when
 * it is executed, then any of the threads in the party could execute that
 * action when it is released. To facilitate this, each invocation of
 * {@link #await} returns the arrival index of that thread at the barrier.
 * You can then choose which thread should execute the barrier action, for
 * example:
 *  <pre> {@code
 * if (barrier.await() == 0) {
 *   // log the completion of this iteration
 * }}</pre>
 *
 * <p>The {@code CyclicBarrier} uses an all-or-none breakage model
 * for failed synchronization attempts: If a thread leaves a barrier
 * point prematurely because of interruption, failure, or timeout, all
 * other threads waiting at that barrier point will also leave
 * abnormally via {@link BrokenBarrierException} (or
 * {@link InterruptedException} if they too were interrupted at about
 * the same time).
 *
 * <p>Memory consistency effects: Actions in a thread prior to calling
 * {@code await()}
 * <a href="package-summary.html#MemoryVisibility"><i>happen-before</i></a>
 * actions that are part of the barrier action, which in turn
 * <i>happen-before</i> actions following a successful return from the
 * corresponding {@code await()} in other threads.
 *
 * @since 1.5
 * @see CountDownLatch
 *
 * @author Doug Lea
 */
public class CyclicBarrier {
    /**
     * Each use of the barrier is represented as a generation instance.
     * The generation changes whenever the barrier is tripped, or
     * is reset. There can be many generations associated with threads
     * using the barrier - due to the non-deterministic way the lock
     * may be allocated to waiting threads - but only one of these
     * can be active at a time (the one to which {@code count} applies)
     * and all the rest are either broken or tripped.
     * There need not be an active generation if there has been a break
     * but no subsequent reset.
     */
    private static class Generation {
        boolean broken = false;
    }
    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();
    /** The number of parties */
    private final int parties;
    /* The command to run when tripped */
    private final Runnable barrierCommand;
    /** The current generation */
    private Generation generation = new Generation();
    /**
     * Number of parties still waiting. Counts down from parties to 0
     * on each generation.  It is reset to parties on each new
     * generation or when broken.
     */
    private int count;
    /**
     * Updates state on barrier trip and wakes up everyone.
     * Called only while holding lock.
     */
    private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }
    /**
     * Sets current barrier generation as broken and wakes up everyone.
     * Called only while holding lock.
     */
    private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }
    /**
     * Main barrier code, covering the various policies.
     */
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            final Generation g = generation;
            if (g.broken)
                throw new BrokenBarrierException();
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
            int index = --count;
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }
            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }
                if (g.broken)
                    throw new BrokenBarrierException();
                if (g != generation)
                    return index;
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }
    /**
     * Creates a new {@code CyclicBarrier} that will trip when the
     * given number of parties (threads) are waiting upon it, and which
     * will execute the given barrier action when the barrier is tripped,
     * performed by the last thread entering the barrier.
     *
     * @param parties the number of threads that must invoke {@link #await}
     *        before the barrier is tripped
     * @param barrierAction the command to execute when the barrier is
     *        tripped, or {@code null} if there is no action
     * @throws IllegalArgumentException if {@code parties} is less than 1
     */
    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }
    /**
     * Creates a new {@code CyclicBarrier} that will trip when the
     * given number of parties (threads) are waiting upon it, and
     * does not perform a predefined action when the barrier is tripped.
     *
     * @param parties the number of threads that must invoke {@link #await}
     *        before the barrier is tripped
     * @throws IllegalArgumentException if {@code parties} is less than 1
     */
    public CyclicBarrier(int parties) {
        this(parties, null);
    }
    /**
     * Returns the number of parties required to trip this barrier.
     *
     * @return the number of parties required to trip this barrier
     */
    public int getParties() {
        return parties;
    }
    /**
     * Waits until all {@linkplain #getParties parties} have invoked
     * {@code await} on this barrier.
     *
     * <p>If the current thread is not the last to arrive then it is
     * disabled for thread scheduling purposes and lies dormant until
     * one of the following things happens:
     * <ul>
     * <li>The last thread arrives; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * one of the other waiting threads; or
     * <li>Some other thread times out while waiting for barrier; or
     * <li>Some other thread invokes {@link #reset} on this barrier.
     * </ul>
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * <p>If the barrier is {@link #reset} while any thread is waiting,
     * or if the barrier {@linkplain #isBroken is broken} when
     * {@code await} is invoked, or while any thread is waiting, then
     * {@link BrokenBarrierException} is thrown.
     *
     * <p>If any thread is {@linkplain Thread#interrupt interrupted} while waiting,
     * then all other waiting threads will throw
     * {@link BrokenBarrierException} and the barrier is placed in the broken
     * state.
     *
     * <p>If the current thread is the last thread to arrive, and a
     * non-null barrier action was supplied in the constructor, then the
     * current thread runs the action before allowing the other threads to
     * continue.
     * If an exception occurs during the barrier action then that exception
     * will be propagated in the current thread and the barrier is placed in
     * the broken state.
     *
     * @return the arrival index of the current thread, where index
     *         {@code getParties() - 1} indicates the first
     *         to arrive and zero indicates the last to arrive
     * @throws InterruptedException if the current thread was interrupted
     *         while waiting
     * @throws BrokenBarrierException if <em>another</em> thread was
     *         interrupted or timed out while the current thread was
     *         waiting, or the barrier was reset, or the barrier was
     *         broken when {@code await} was called, or the barrier
     *         action (if present) failed due to an exception
     */
    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }
    /**
     * Waits until all {@linkplain #getParties parties} have invoked
     * {@code await} on this barrier, or the specified waiting time elapses.
     *
     * <p>If the current thread is not the last to arrive then it is
     * disabled for thread scheduling purposes and lies dormant until
     * one of the following things happens:
     * <ul>
     * <li>The last thread arrives; or
     * <li>The specified timeout elapses; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * one of the other waiting threads; or
     * <li>Some other thread times out while waiting for barrier; or
     * <li>Some other thread invokes {@link #reset} on this barrier.
     * </ul>
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * <p>If the specified waiting time elapses then {@link TimeoutException}
     * is thrown. If the time is less than or equal to zero, the
     * method will not wait at all.
     *
     * <p>If the barrier is {@link #reset} while any thread is waiting,
     * or if the barrier {@linkplain #isBroken is broken} when
     * {@code await} is invoked, or while any thread is waiting, then
     * {@link BrokenBarrierException} is thrown.
     *
     * <p>If any thread is {@linkplain Thread#interrupt interrupted} while
     * waiting, then all other waiting threads will throw {@link
     * BrokenBarrierException} and the barrier is placed in the broken
     * state.
     *
     * <p>If the current thread is the last thread to arrive, and a
     * non-null barrier action was supplied in the constructor, then the
     * current thread runs the action before allowing the other threads to
     * continue.
     * If an exception occurs during the barrier action then that exception
     * will be propagated in the current thread and the barrier is placed in
     * the broken state.
     *
     * @param timeout the time to wait for the barrier
     * @param unit the time unit of the timeout parameter
     * @return the arrival index of the current thread, where index
     *         {@code getParties() - 1} indicates the first
     *         to arrive and zero indicates the last to arrive
     * @throws InterruptedException if the current thread was interrupted
     *         while waiting
     * @throws TimeoutException if the specified timeout elapses.
     *         In this case the barrier will be broken.
     * @throws BrokenBarrierException if <em>another</em> thread was
     *         interrupted or timed out while the current thread was
     *         waiting, or the barrier was reset, or the barrier was broken
     *         when {@code await} was called, or the barrier action (if
     *         present) failed due to an exception
     */
    public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }
    /**
     * Queries if this barrier is in a broken state.
     *
     * @return {@code true} if one or more parties broke out of this
     *         barrier due to interruption or timeout since
     *         construction or the last reset, or a barrier action
     *         failed due to an exception; {@code false} otherwise.
     */
    public boolean isBroken() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return generation.broken;
        } finally {
            lock.unlock();
        }
    }
    /**
     * Resets the barrier to its initial state.  If any parties are
     * currently waiting at the barrier, they will return with a
     * {@link BrokenBarrierException}. Note that resets <em>after</em>
     * a breakage has occurred for other reasons can be complicated to
     * carry out; threads need to re-synchronize in some other way,
     * and choose one to perform the reset.  It may be preferable to
     * instead create a new barrier for subsequent use.
     */
    public void reset() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            breakBarrier();   // break the current generation
            nextGeneration(); // start a new generation
        } finally {
            lock.unlock();
        }
    }
    /**
     * Returns the number of parties currently waiting at the barrier.
     * This method is primarily useful for debugging and assertions.
     *
     * @return the number of parties currently blocked in {@link #await}
     */
    public int getNumberWaiting() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return parties - count;
        } finally {
            lock.unlock();
        }
    }
}


ReentrantLock用法

注明:Condition.signal/signalAll的作用等同于Object.notify/notifyAll,Conditon.await等同于Object.wait,所以下面的论述全部依靠object.wait和notify来完成了,因为实在懒得写了

1.sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll,被interrupt或者等待超时后本线程才能才进入对象锁定池准备获得对象锁进入运行状态。

2.Thread.sleep与Object.wait的差别

  (1)sleep属于Thread的方法和wait属于Object的方法

  (2)sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者同步方法。

  (3)wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)

    synchronized(x){

   x.notify()

  //或者wait()

   }

  (4)sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常,这儿说的并不是检查性异常(sleep和wait都会抛出检查型异常InterruptedException),这儿只是编码习惯和一般规定

3.关于sleep和wait的一些更深的理解

  (1)sleep和wait一定是由于外部对该线程调用了interrupt方法才抛出的InterruptedException 

 (2)对于wait而言,如果要向上抛出InterruptedException(不管是不是在同步区之内)或者是要直接在catch当中处理,都必须要等待其它线程释放了锁资源,该线程获取到锁资源才能处理

  (3)所以在wait当中,对于InterruptedException异常的抛出和捕获(catch)异常都属于临界区,必须要获取到锁资源才能进行这两个操作

  相当于interrupt方法免除了notify/notifyAll这一个步骤,不过含有抛出InterruptedException的副作用

  (4)wait(int timeout)是有限等待,在等待时间之内收到了notify/notifyall.或者当等待超过了timeout的时间,该线程会自动的开始尝试获取锁资源,在这段时间如果被interrupt参考上面两条

  (5)总之对于锁所保护的临界区而言,里面所产生的任何代码和抛出异常的操作都必须要在该线程获取到锁资源后才能进行,无论是独占还是共享锁(Concurrent包)

  看看下面的代码可以理解:

   public class Main2 {

private static final Object locker=new Object();

public static  void  main(String[]args) {

ThreadA a=new ThreadA();

ThreadB b=new ThreadB();

a.start();

b.start();

b.interrupt();

}

static final class  ThreadA extends Thread{

@Override

public void run() {

synchronized (locker) {

for(int i=0;i<20;i++){

System.out.println(false);

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

}

static final class  ThreadB extends Thread{

@Override

public void run() {

synchronized (locker) {

try {

locker.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

for(int i=0;i<20;i++){

System.out.println(true);

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

}

ReentrantLock获取锁定的三种方式:

    a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁

    b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;

    c) tryLock(long timeout,TimeUnit unit),   如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;

     d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断

原文

地址:http://blog.csdn.net/vernonzheng/article/details/8288251

一、ReentrantLock 类


1.1 什么是reentrantlock


java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

reentrant 锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。

1.2 ReentrantLock与synchronized的比较


相同:ReentrantLock提供了synchronized类似的功能和内存语义。
不同:
(1)ReentrantLock功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock更合适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性。
(2)ReentrantLock 的性能比synchronized会好点。
(3)ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。

1.3 ReentrantLock扩展的功能


1.3.1 实现可轮询的锁请求 


在内部锁中,死锁是致命的——唯一的恢复方法是重新启动程序,唯一的预防方法是在构建程序时不要出错。而可轮询的锁获取模式具有更完善的错误恢复机制,可以规避死锁的发生。 
如果你不能获得所有需要的锁,那么使用可轮询的获取方式使你能够重新拿到控制权,它会释放你已经获得的这些锁,然后再重新尝试。可轮询的锁获取模式,由tryLock()方法实现。此方法仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值true。如果锁不可用,则此方法将立即返回值false。此方法的典型使用语句如下: 
[java] view plain copy
  1. Lock lock = ...;   
  2. if (lock.tryLock()) {   
  3. try {   
  4. // manipulate protected state   
  5. finally {   
  6. lock.unlock();   
  7. }   
  8. else {   
  9. // perform alternative actions   
  10. }   

1.3.2 实现可定时的锁请求 


当使用内部锁时,一旦开始请求,锁就不能停止了,所以内部锁给实现具有时限的活动带来了风险。为了解决这一问题,可以使用定时锁。当具有时限的活 
动调用了阻塞方法,定时锁能够在时间预算内设定相应的超时。如果活动在期待的时间内没能获得结果,定时锁能使程序提前返回。可定时的锁获取模式,由tryLock(long, TimeUnit)方法实现。 

1.3.3 实现可中断的锁获取请求 


可中断的锁获取操作允许在可取消的活动中使用。lockInterruptibly()方法能够使你获得锁的时候响应中断。

1.4 ReentrantLock不好与需要注意的地方


(1) lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会获得自动释放
(2) 当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。

二、条件变量Condition


条件变量很大一个程度上是为了解决Object.wait/notify/notifyAll难以使用的问题。
条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。
上述API说明表明条件变量需要与锁绑定,而且多个Condition需要绑定到同一锁上。前面的Lock中提到,获取一个条件变量的方法是Lock.newCondition()
[java] view plain copy
  1. void await() throws InterruptedException;  
  2.   
  3. void awaitUninterruptibly();  
  4.   
  5. long awaitNanos(long nanosTimeout) throws InterruptedException;  
  6.   
  7. boolean await(long time, TimeUnit unit) throws InterruptedException;  
  8.   
  9. boolean awaitUntil(Date deadline) throws InterruptedException;  
  10.   
  11. void signal();  
  12.   
  13. void signalAll();  


以上是Condition接口定义的方法,await*对应于Object.waitsignal对应于Object.notifysignalAll对应于Object.notifyAll。特别说明的是Condition的接口改变名称就是为了避免与Object中的wait/notify/notifyAll的语义和使用上混淆,因为Condition同样有wait/notify/notifyAll方法。
每一个Lock可以有任意数据的Condition对象,Condition是与Lock绑定的,所以就有Lock的公平性特性:如果是公平锁,线程为按照FIFO的顺序从Condition.await中释放,如果是非公平锁,那么后续的锁竞争就不保证FIFO顺序了。
一个使用Condition实现生产者消费者的模型例子如下。
[java] view plain copy
  1. import java.util.concurrent.locks.Condition;  
  2. import java.util.concurrent.locks.Lock;  
  3. import java.util.concurrent.locks.ReentrantLock;  
  4.   
  5. public class ProductQueue<T> {  
  6.   
  7.     private final T[] items;  
  8.   
  9.     private final Lock lock = new ReentrantLock();  
  10.   
  11.     private Condition notFull = lock.newCondition();  
  12.   
  13.     private Condition notEmpty = lock.newCondition();  
  14.   
  15.     //  
  16.     private int head, tail, count;  
  17.   
  18.     public ProductQueue(int maxSize) {  
  19.         items = (T[]) new Object[maxSize];  
  20.     }  
  21.   
  22.     public ProductQueue() {  
  23.         this(10);  
  24.     }  
  25.   
  26.     public void put(T t) throws InterruptedException {  
  27.         lock.lock();  
  28.         try {  
  29.             while (count == getCapacity()) {  
  30.                 notFull.await();  
  31.             }  
  32.             items[tail] = t;  
  33.             if (++tail == getCapacity()) {  
  34.                 tail = 0;  
  35.             }  
  36.             ++count;  
  37.             notEmpty.signalAll();  
  38.         } finally {  
  39.             lock.unlock();  
  40.         }  
  41.     }  
  42.   
  43.     public T take() throws InterruptedException {  
  44.         lock.lock();  
  45.         try {  
  46.             while (count == 0) {  
  47.                 notEmpty.await();  
  48.             }  
  49.             T ret = items[head];  
  50.             items[head] = null;//GC  
  51.             //  
  52.             if (++head == getCapacity()) {  
  53.                 head = 0;  
  54.             }  
  55.             --count;  
  56.             notFull.signalAll();  
  57.             return ret;  
  58.         } finally {  
  59.             lock.unlock();  
  60.         }  
  61.     }  
  62.   
  63.     public int getCapacity() {  
  64.         return items.length;  
  65.     }  
  66.   
  67.     public int size() {  
  68.         lock.lock();  
  69.         try {  
  70.             return count;  
  71.         } finally {  
  72.             lock.unlock();  
  73.         }  
  74.     }  
  75.   
  76. }  


在这个例子中消费take()需要 队列不为空,如果为空就挂起(await()),直到收到notEmpty的信号;生产put()需要队列不满,如果满了就挂起(await()),直到收到notFull的信号。
可能有人会问题,如果一个线程lock()对象后被挂起还没有unlock,那么另外一个线程就拿不到锁了(lock()操作会挂起),那么就无法通知(notify)前一个线程,这样岂不是“死锁”了?
 

2.1 await* 操作


上一节中说过多次ReentrantLock是独占锁,一个线程拿到锁后如果不释放,那么另外一个线程肯定是拿不到锁,所以在lock.lock()lock.unlock()之间可能有一次释放锁的操作(同样也必然还有一次获取锁的操作)。我们再回头看代码,不管take()还是put(),在进入lock.lock()后唯一可能释放锁的操作就是await()了。也就是说await()操作实际上就是释放锁,然后挂起线程,一旦条件满足就被唤醒,再次获取锁!
[java] view plain copy
  1. public final void await() throws InterruptedException {  
  2.     if (Thread.interrupted())  
  3.         throw new InterruptedException();  
  4.     Node node = addConditionWaiter();  
  5.     int savedState = fullyRelease(node);  
  6.     int interruptMode = 0;  
  7.     while (!isOnSyncQueue(node)) {  
  8.         LockSupport.park(this);  
  9.         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)  
  10.             break;  
  11.     }  
  12.     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)  
  13.         interruptMode = REINTERRUPT;  
  14.     if (node.nextWaiter != null)  
  15.         unlinkCancelledWaiters();  
  16.     if (interruptMode != 0)  
  17.         reportInterruptAfterWait(interruptMode);  
  18. }  


上面是await()的代码片段。上一节中说过,AQS在获取锁的时候需要有一个CHL的FIFO队列,所以对于一个Condition.await()而言,如果释放了锁,要想再一次获取锁那么就需要进入队列,等待被通知获取锁。完整的await()操作是安装如下步骤进行的:
  1. 将当前线程加入Condition锁队列。特别说明的是,这里不同于AQS的队列,这里进入的是Condition的FIFO队列。后面会具体谈到此结构。进行2。
  2. 释放锁。这里可以看到将锁释放了,否则别的线程就无法拿到锁而发生死锁。进行3。
  3. 自旋(while)挂起,直到被唤醒或者超时或者CACELLED等。进行4。
  4. 获取锁(acquireQueued)。并将自己从Condition的FIFO队列中释放,表明自己不再需要锁(我已经拿到锁了)。
这里再回头介绍Condition数据结构。我们知道一个Condition可以在多个地方被await*(),那么就需要一个FIFO的结构将这些Condition串联起来,然后根据需要唤醒一个或者多个(通常是所有)。所以在Condition内部就需要一个FIFO的队列。
[java] view plain copy
  1. private transient Node firstWaiter;  
  2. private transient Node lastWaiter;  

上面的两个节点就是描述一个FIFO的队列。我们再结合前面提到的节点(Node)数据结构。我们就发现Node.nextWaiter就派上用场了!nextWaiter就是将一系列的Condition.await*串联起来组成一个FIFO的队列。
 

2.2 signal/signalAll 操作


await*()清楚了,现在再来看signal/signalAll就容易多了。按照signal/signalAll的需求,就是要将Condition.await*()中FIFO队列中第一个Node唤醒(或者全部Node)唤醒。尽管所有Node可能都被唤醒,但是要知道的是仍然只有一个线程能够拿到锁,其它没有拿到锁的线程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。
[java] view plain copy
  1. private void doSignal(Node first) {  
  2.     do {  
  3.         if ( (firstWaiter = first.nextWaiter) == null)  
  4.             lastWaiter = null;  
  5.         first.nextWaiter = null;  
  6.     } while (!transferForSignal(first) &&  
  7.              (first = firstWaiter) != null);  
  8. }  
  9.   
  10. private void doSignalAll(Node first) {  
  11.     lastWaiter = firstWaiter  = null;  
  12.     do {  
  13.         Node next = first.nextWaiter;  
  14.         first.nextWaiter = null;  
  15.         transferForSignal(first);  
  16.         first = next;  
  17.     } while (first != null);  
  18. }  


上面的代码很容易看出来,signal就是唤醒Condition队列中的第一个非CANCELLED节点线程,而signalAll就是唤醒所有非CANCELLED节点线程。当然了遇到CANCELLED线程就需要将其从FIFO队列中剔除。
[java] view plain copy
  1. final boolean transferForSignal(Node node) {  
  2.     if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))  
  3.         return false;  
  4.   
  5.     Node p = enq(node);  
  6.     int c = p.waitStatus;  
  7.     if (c > 0 || !compareAndSetWaitStatus(p, c, Node.SIGNAL))  
  8.         LockSupport.unpark(node.thread);  
  9.     return true;  
  10. }  


上面就是唤醒一个await*()线程的过程,根据前面的小节介绍的,如果要unpark线程,并使线程拿到锁,那么就需要线程节点进入AQS的队列。所以可以看到在LockSupport.unpark之前调用了enq(node)操作,将当前节点加入到AQS队列。



ReentrantLock源码解析
ReentrantLock原理个人理解
ReentrantLock内部含有一个AbstractQueuedSynchronizer的同步器实现sync,包括公平和不公平两个模式如下:
    //抽象队列同步器
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
        //抽象锁操作 留给子类实现
        abstract void lock();
        //尝试不公平获取资源 就是不管队列是否为空,都会先尝试获取资源,获取成功后设置独占线程
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
        //尝试释放资源
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
        //当前线程是否独占资源
        protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
        //实例化个新的Condition
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
        //获取当前占有资源的线程
        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }
        //获取当前线程占有的资源数量
        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }
        //判断资源是否等于0 如果不等于0 资源就被锁住 否则资源时空闲的
        final boolean isLocked() {
            return getState() != 0;
        }
    }

    //不公平抽象队列同步器
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        //锁 先尝试直接获取资源,否则调用acquire获取资源
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        //使用父类不公平的方式尝试获取资源
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    //公平抽象队列同步器
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        //锁。直接调用acquire获取资源
        final void lock() {
            acquire(1);
        }

        //尝试获取资源,会照顾队列前的元素 获取到资源后设置独占线程
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

ReentrantLock外部接口
    //获取锁
    public void lock() {
        sync.lock();
    }
    //释放锁
      public void unlock() {
        sync.release(1);
    }

over

参考:
《深入浅出 Java Concurrency》—锁机制(一)Lock与ReentrantLock
http://blog.csdn.net/fg2006/article/details/6397894
Java多线程基础总结七:ReentrantLock(2)
http://www.bianceng.cn/Programming/Java/201206/34155_2.htm
再谈重入锁--ReentrantLock
http://tenyears.iteye.com/blog/48750
深入浅出 Java Concurrency (9): 锁机制 part 4
http://www.blogjava.net/xylz/archive/2010/07/08/325540.html

个人理解
想想一个需求,现在有N多个任务需要执行,在这些任务执行每个完成时需要立即对其结果进行进一步处理,这个时候应该怎么做?仔细想想除非显示的去修改任务的代码,否则没有好的办法可以完成。这时CompetionService可以登场了!
CompletionService接口定义了一组带相同返回值的任务的管理接口,可以最快的获取这些批量任务的执行结果,ExecutorService.invokeAny就使用了这个技术!
    源码如下:
    public interface CompletionService<V> {
    //提交任务
    Future<V> submit(Callable<V> task);

    //提交任务
    Future<V> submit(Runnable task, V result);

    //获取一个执行结果(并移除) 阻塞
    Future<V> take() throws InterruptedException;

    //获取一个执行结果(并移除) 不阻塞
    Future<V> poll();

    //获取一个执行结果(并移除) 带超时
    Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
     }
ExecutorCompletionService类是CompletionService接口的实现
ExecutorCompletionService内部管理者一个已完成任务的阻塞队列,其引用了一个Executor用来执行任务,submit()方法最终会委托给内部的executor去执行任务,take/poll方法的工作都委托给内部的已完成任务阻塞队列
    如果阻塞队列中有已完成的任务, take方法就返回任务的结果, 否则阻塞等待任务完成
    源码如下:
    public class ExecutorCompletionService<V> implements CompletionService<V> {
            //被包装Executor
    private final Executor executor;
    //如果executor是AbstractExecutorService就为它
    private final AbstractExecutorService aes;
    //完成任务的阻塞队列
    private final BlockingQueue<Future<V>> completionQueue;

    /**
     * 扩展FutureTask,重写了其done方法用于将完成任务放置在阻塞队列!!!
     */
    private class QueueingFuture extends FutureTask<Void> {
        QueueingFuture(RunnableFuture<V> task) {
            super(task, null);
            this.task = task;
        }
        protected void done() { completionQueue.add(task); }
        private final Future<V> task;
    }

    private RunnableFuture<V> newTaskFor(Callable<V> task) {
        if (aes == null)
            return new FutureTask<V>(task);
        else
            return aes.newTaskFor(task);
    }

    private RunnableFuture<V> newTaskFor(Runnable task, V result) {
        if (aes == null)
            return new FutureTask<V>(task, result);
        else
            return aes.newTaskFor(task, result);
    }

    
    public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
this.completionQueue = new LinkedBlockingQueue<Future<V>>();
    }

   
    public ExecutorCompletionService(Executor executor,
                                     BlockingQueue<Future<V>> completionQueue) {
        if (executor == null || completionQueue == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
this.completionQueue = completionQueue;
    }

    //提交任务
    public Future<V> submit(Callable<V> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task);
        executor.execute(new QueueingFuture(f));
        return f;
    }

    //提交任务
    public Future<V> submit(Runnable task, V result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task, result);
        executor.execute(new QueueingFuture(f));
        return f;
    }

    //获取结果带阻塞
    public Future<V> take() throws InterruptedException {
        return completionQueue.take();
    }

     //获取结果
    public Future<V> poll() {
        return completionQueue.poll();
    }

    //获取结果 带超时
    public Future<V> poll(long timeout, TimeUnit unit)
            throws InterruptedException {
        return completionQueue.poll(timeout, unit);
    }

}
       ExecutorCompletionService主要用与管理一组带相同返回结果类型的异步任务 (有结果的任务, 任务完成后要处理结果)

原文
地址:
  1. CompletionService接口定义了一组任务管理接口:
  1. ExecutorCompletionService类是CompletionService接口的实现
  1. 关于CompletionService和ExecutorCompletionService的类图如下:
CompletionService.png
下面是简单的用法:
package com.example.concurrent;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {

public static void main(String[] args) throws ExecutionException, InterruptedException {
// case1();// case2();
case3();
}


/**
* <一>
* 1. 用List收集任务结果 (List记录每个submit返回的Future)
* 2. 循环查看结果, Future不一定完成, 如果没有完成, 那么调用get会租塞
* 3. 如果排在前面的任务没有完成, 那么就会阻塞, 这样后面已经完成的任务就没法获得结果了, 导致了不必要的等待时间.
* 更为严重的是: 第一个任务如果几个小时或永远完成不了, 而后面的任务几秒钟就完成了, 那么后面的任务的结果都将得不到处理
*
* 导致: 已完成的任务可能得不到及时处理
*/
private static void case1() throws ExecutionException, InterruptedException {
final Random random = new Random();
ExecutorService service = Executors.newFixedThreadPool(10);
List<Future<String>> taskResultHolder = new ArrayList<>();
for(int i=0; i<50; i++) {
//搜集任务结果
taskResultHolder.add(service.submit(new Callable<String>() {
public String call() throws Exception {
Thread.sleep(random.nextInt(5000));
return Thread.currentThread().getName();
}
}));
}
// 处理任务结果
int count = 0;
System.out.println("handle result begin");
for(Future<String> future : taskResultHolder) {
System.out.println(future.get());
count++;
}
System.out.println("handle result end");
System.out.println(count + " task done !");

//关闭线程池
service.shutdown();
}

/**
* <二> 只对第一种情况进行的改进
* 1. 查看任务是否完成, 如果完成, 就获取任务的结果, 让后重任务列表中删除任务.
* 2. 如果任务未完成, 就跳过此任务, 继续查看下一个任务结果.
* 3. 如果到了任务列表末端, 那么就从新回到任务列表开始, 然后继续从第一步开始执行
*
* 这样就可以及时处理已完成任务的结果了
*/
private static void case2() throws ExecutionException, InterruptedException {
final Random random = new Random();
ExecutorService service = Executors.newFixedThreadPool(10);
List<Future<String>> results = new ArrayList<>();

for(int i=0; i<50; i++) {
Callable<String> task = new Callable<String>() {
public String call() throws Exception {
Thread.sleep(random.nextInt(5000)); //模拟耗时操作
return Thread.currentThread().getName();
}
};
Future<String> future = service.submit(task);
results.add(future); // 搜集任务结果
}

int count = 0;
//自旋, 获取结果
System.out.println("handle result begin");
for(int i=0; i<results.size(); i++) {
Future<String> taskHolder = results.get(i);

if(taskHolder.isDone()) { //任务完成
String result = taskHolder.get(); //获取结果, 进行某些操作
System.out.println("result: " + result);
results.remove(taskHolder);
i--;

count++; //完成的任务的计数器
}

//回到列表开头, 从新获取结果
if(i == results.size() - 1) i = -1;
}
System.out.println("handle result end");
System.out.println(count + " task done !");

//线程池使用完必须关闭
service.shutdown();
}


/**
* <三> 使用ExecutorCompletionService管理异步任务
* 1. Java中的ExecutorCompletionService<V>本身有管理任务队列的功能
* i. ExecutorCompletionService内部维护列一个队列, 用于管理已完成的任务
* ii. 内部还维护列一个Executor, 可以执行任务
*
* 2. ExecutorCompletionService内部维护了一个BlockingQueue, 只有完成的任务才被加入到队列中
*
* 3. 任务一完成就加入到内置管理队列中, 如果队列中的数据为空时, 调用take()就会阻塞 (等待任务完成)
* i. 关于完成任务是如何加入到完成队列中的, 请参考ExecutorCompletionService的内部类QueueingFuture的done()方法
*
* 4. ExecutorCompletionService的take/poll方法是对BlockingQueue对应的方法的封装, 关于BlockingQueue的take/poll方法:
* i. take()方法, 如果队列中有数据, 就返回数据, 否则就一直阻塞;
* ii. poll()方法: 如果有值就返回, 否则返回null
* iii. poll(long timeout, TimeUnit unit)方法: 如果有值就返回, 否则等待指定的时间; 如果时间到了如果有值, 就返回值, 否则返回null
*
* 解决了已完成任务得不到及时处理的问题
*/
static void case3() throws InterruptedException, ExecutionException {
Random random = new Random();

ExecutorService service = Executors.newFixedThreadPool(10);
ExecutorCompletionService<String> completionService = new ExecutorCompletionService<String>(service);

for(int i=0; i<50; i++) {
completionService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(random.nextInt(5000));
return Thread.currentThread().getName();
}
});
}

int completionTask = 0;
while(completionTask < 50) {
//如果完成队列中没有数据, 则阻塞; 否则返回队列中的数据
Future<String> resultHolder = completionService.take();
System.out.println("result: " + resultHolder.get());
completionTask++;
}

System.out.println(completionTask + " task done !");

//ExecutorService使用完一定要关闭 (回收资源, 否则系统资源耗尽! .... 呵呵...)
service.shutdown();
}
}
那咩, ExecutorCompletionService是如何执行任务, 又是如何将任务的结果存储到完成队列中的呢?
  1. ExecutorCompletionService在submit任务时, 会创建一个QueueingFuture, 然后将创建的QueueingFuture丢给executor, 让executor完成任务的执行工作
  2. QueueingFuture继承与FutureTask类, 而FutureTask实现了两个接口Runnable和Future.
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
protected void done() {
completionQueue.add(task);
}
正式在此方法中把执行完的任务放置到完成队列中的!!
然后我们就可以在submit线程中从完成队列中取出任务句柄, 获取任务结果了!!!
至此, ExecutorCompletionService原理已经解析完毕 !!
欲知后事如何, 切听下回分解 !!
==> Future/FutureTask源码分析


作者:元亨利贞o
链接:https://www.jianshu.com/p/cfda708a3478
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

几个方法
 boolean add(E e); 添加元素,不阻塞,如果队列已满,抛出异常
boolean offer(E e); 添加元素,不阻塞 如果队列已满,返回false,成功返回true
boolean offer(E e, long timeout, TimeUnit unit)  throws InterruptedException;添加元素,限时等待,成功返回true,失败false
 void put(E e) throws InterruptedException;阻塞添加元素
E remove(); 移出元素,不阻塞,如果队列为空,抛出异常
 E poll();移出元素,队列为空则false,否则true
E poll(long timeout, TimeUnit unit) 移出元素,有限等待
        throws InterruptedException;
E take() throws InterruptedException;阻塞移出元素
E element();获取元素,队列为空抛出NosuchElement异常
E peek();获取元素,队列为空,返回null
总体来说:
put和take是阻塞版本
offer和poll是不抛出异常,且支持不等待和有限时间等待版本
add和remove是抛出异常,不等待版本

源码如下:
public interface BlockingQueue<E> extends Queue<E> {
    /**
     * Inserts the specified element into this queue if it is possible to do
     * so immediately without violating capacity restrictions, returning
     * {@code true} upon success and throwing an
     * {@code IllegalStateException} if no space is currently available.
     * When using a capacity-restricted queue, it is generally preferable to
     * use {@link #offer(Object) offer}.
     *
     * @param e the element to add
     * @return {@code true} (as specified by {@link Collection#add})
     * @throws IllegalStateException if the element cannot be added at this
     *         time due to capacity restrictions
     * @throws ClassCastException if the class of the specified element
     *         prevents it from being added to this queue
     * @throws NullPointerException if the specified element is null
     * @throws IllegalArgumentException if some property of the specified
     *         element prevents it from being added to this queue
     */
    boolean add(E e);
    /**
     * Inserts the specified element into this queue if it is possible to do
     * so immediately without violating capacity restrictions, returning
     * {@code true} upon success and {@code false} if no space is currently
     * available.  When using a capacity-restricted queue, this method is
     * generally preferable to {@link #add}, which can fail to insert an
     * element only by throwing an exception.
     *
     * @param e the element to add
     * @return {@code true} if the element was added to this queue, else
     *         {@code false}
     * @throws ClassCastException if the class of the specified element
     *         prevents it from being added to this queue
     * @throws NullPointerException if the specified element is null
     * @throws IllegalArgumentException if some property of the specified
     *         element prevents it from being added to this queue
     */
    boolean offer(E e);
    /**
     * Inserts the specified element into this queue, waiting if necessary
     * for space to become available.
     *
     * @param e the element to add
     * @throws InterruptedException if interrupted while waiting
     * @throws ClassCastException if the class of the specified element
     *         prevents it from being added to this queue
     * @throws NullPointerException if the specified element is null
     * @throws IllegalArgumentException if some property of the specified
     *         element prevents it from being added to this queue
     */
    void put(E e) throws InterruptedException;
    /**
     * Inserts the specified element into this queue, waiting up to the
     * specified wait time if necessary for space to become available.
     *
     * @param e the element to add
     * @param timeout how long to wait before giving up, in units of
     *        {@code unit}
     * @param unit a {@code TimeUnit} determining how to interpret the
     *        {@code timeout} parameter
     * @return {@code true} if successful, or {@code false} if
     *         the specified waiting time elapses before space is available
     * @throws InterruptedException if interrupted while waiting
     * @throws ClassCastException if the class of the specified element
     *         prevents it from being added to this queue
     * @throws NullPointerException if the specified element is null
     * @throws IllegalArgumentException if some property of the specified
     *         element prevents it from being added to this queue
     */
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;
    /**
     * Retrieves and removes the head of this queue, waiting if necessary
     * until an element becomes available.
     *
     * @return the head of this queue
     * @throws InterruptedException if interrupted while waiting
     */
    E take() throws InterruptedException;
    /**
     * Retrieves and removes the head of this queue, waiting up to the
     * specified wait time if necessary for an element to become available.
     *
     * @param timeout how long to wait before giving up, in units of
     *        {@code unit}
     * @param unit a {@code TimeUnit} determining how to interpret the
     *        {@code timeout} parameter
     * @return the head of this queue, or {@code null} if the
     *         specified waiting time elapses before an element is available
     * @throws InterruptedException if interrupted while waiting
     */
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;
    /**
     * Returns the number of additional elements that this queue can ideally
     * (in the absence of memory or resource constraints) accept without
     * blocking, or {@code Integer.MAX_VALUE} if there is no intrinsic
     * limit.
     *
     * <p>Note that you <em>cannot</em> always tell if an attempt to insert
     * an element will succeed by inspecting {@code remainingCapacity}
     * because it may be the case that another thread is about to
     * insert or remove an element.
     *
     * @return the remaining capacity
     */
    int remainingCapacity();
    /**
     * Removes a single instance of the specified element from this queue,
     * if it is present.  More formally, removes an element {@code e} such
     * that {@code o.equals(e)}, if this queue contains one or more such
     * elements.
     * Returns {@code true} if this queue contained the specified element
     * (or equivalently, if this queue changed as a result of the call).
     *
     * @param o element to be removed from this queue, if present
     * @return {@code true} if this queue changed as a result of the call
     * @throws ClassCastException if the class of the specified element
     *         is incompatible with this queue
     *         (<a href="../Collection.html#optional-restrictions">optional</a>)
     * @throws NullPointerException if the specified element is null
     *         (<a href="../Collection.html#optional-restrictions">optional</a>)
     */
    boolean remove(Object o);
    /**
     * Returns {@code true} if this queue contains the specified element.
     * More formally, returns {@code true} if and only if this queue contains
     * at least one element {@code e} such that {@code o.equals(e)}.
     *
     * @param o object to be checked for containment in this queue
     * @return {@code true} if this queue contains the specified element
     * @throws ClassCastException if the class of the specified element
     *         is incompatible with this queue
     *         (<a href="../Collection.html#optional-restrictions">optional</a>)
     * @throws NullPointerException if the specified element is null
     *         (<a href="../Collection.html#optional-restrictions">optional</a>)
     */
    public boolean contains(Object o);
    /**
     * Removes all available elements from this queue and adds them
     * to the given collection.  This operation may be more
     * efficient than repeatedly polling this queue.  A failure
     * encountered while attempting to add elements to
     * collection {@code c} may result in elements being in neither,
     * either or both collections when the associated exception is
     * thrown.  Attempts to drain a queue to itself result in
     * {@code IllegalArgumentException}. Further, the behavior of
     * this operation is undefined if the specified collection is
     * modified while the operation is in progress.
     *
     * @param c the collection to transfer elements into
     * @return the number of elements transferred
     * @throws UnsupportedOperationException if addition of elements
     *         is not supported by the specified collection
     * @throws ClassCastException if the class of an element of this queue
     *         prevents it from being added to the specified collection
     * @throws NullPointerException if the specified collection is null
     * @throws IllegalArgumentException if the specified collection is this
     *         queue, or some property of an element of this queue prevents
     *         it from being added to the specified collection
     */
    int drainTo(Collection<? super E> c);
    /**
     * Removes at most the given number of available elements from
     * this queue and adds them to the given collection.  A failure
     * encountered while attempting to add elements to
     * collection {@code c} may result in elements being in neither,
     * either or both collections when the associated exception is
     * thrown.  Attempts to drain a queue to itself result in
     * {@code IllegalArgumentException}. Further, the behavior of
     * this operation is undefined if the specified collection is
     * modified while the operation is in progress.
     *
     * @param c the collection to transfer elements into
     * @param maxElements the maximum number of elements to transfer
     * @return the number of elements transferred
     * @throws UnsupportedOperationException if addition of elements
     *         is not supported by the specified collection
     * @throws ClassCastException if the class of an element of this queue
     *         prevents it from being added to the specified collection
     * @throws NullPointerException if the specified collection is null
     * @throws IllegalArgumentException if the specified collection is this
     *         queue, or some property of an element of this queue prevents
     *         it from being added to the specified collection
     */
    int drainTo(Collection<? super E> c, int maxElements);
}


个人理解
一.Fork/Join框架的概念
  Fork/Join框架是Java7提供的一个用于并行执行任务的框架,核心是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架
二.工作窃取算法
  工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。目的是为了防止线程空闲,从而提高并发性,Fork/Join框架是通过将每个线程分配的任务通过双端队列来存储而实现的
三.核心API
  [1]ForkJoinTask:任务抽象类,一个ForkJoinTask代表一个任务,
  [2]RecursiveAction:不含返回结果的任务抽象类,继承自ForkJoinTask,用于没有返回结果的任务。
  [3]RecursiveTask :含有返回结果的任务抽象类,继承自ForkJoinTask,用于有返回结果的任务。
  [4]ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。
四.核心方法
  ForkJoinTask.fork方法:将该子任务添加到当前任务所在的ForkJoinPool中进行执行。
  ForkJoinTask.join方法:等待子任务执行完毕
  ForkJoinPool.submit方法:提交第一个根子任务
五.使用流程
  继承自相关任务抽象类。重写compute方法,其中需要完成相关的切分合并逻辑,将根任务交给ForkJoinPool执行
六.注意事项
  1.Fork/Join不适用于计算带有特殊依赖性的情况!!!
七.小demo
  //通过Fork/Join并行计算1到n的和
  public class App {
public static void main(String []args) throws InterruptedException, ExecutionException{
ForkJoinPool forkJoinPool=new ForkJoinPool();
CountTask countTask=new CountTask(0, 212918392);
Future<Integer> result=forkJoinPool.submit(countTask);
System.out.print(result.get());
}
  }
  public class CountTask extends RecursiveTask<Integer> {
private static final long serialVersionUID = 4699632421974478868L;
private static final int THRESHOLD=2;//阈值
private final int start;
private final int end;
public CountTask(int start,int end) {
this.start=start;
this.end=end;
}
@Override
protected Integer compute() {
int sum=0;
//如果任务足够小就计算任务
boolean canCompute=(end-start)<=THRESHOLD;
if(canCompute){
for(int i=start;i<=end;i++){
sum+=i;
}
}else{//如果任务大于阈值,就分裂成两个子任务计算
int middle=(start+end)>>1;
CountTask leftTask=new CountTask(start, middle);
CountTask rightTask=new CountTask(middle, end);
leftTask.fork();
rightTask.fork();
int leftResult=leftTask.join();
int rightResult=rightTask.join();
sum=leftResult+rightResult;
}
return sum;
}
   }
源码解析
Fork/Join框架的核心类
ForkJoinPool:ForkJoinPool是Fork-Join的任务执行线程池,是框架的核心,submit方法可以提交任务(一般提交一个根 任务就OK
ForkJoinWorkerThread:工作者线程,每个工作者线程都有ForkJoinPool的引用(回调)和一个workQueue(工作队列,可被其它线程窃取)
ForkJoinTask:代表一个ForkJoin任务,核心是fork方法(将当任务加到工作队列),join方法(阻塞获取任务结果),invoke方法是阻塞执行(一般不会怎么使用)
RecursiveAction :继承ForkJoinTask和实现exec方法,exec方法调用compute方法,无返回值。
RecursiveTask:继承ForkJoinTask和实现exec方法,exec方法调用compute方法,有返回值。
源码如下:
ForkJoinPool.java
/**
 * An {@link ExecutorService} for running {@link ForkJoinTask}s.
 * A {@code ForkJoinPool} provides the entry point for submissions
 * from non-{@code ForkJoinTask} clients, as well as management and
 * monitoring operations.
 *
 * <p>A {@code ForkJoinPool} differs from other kinds of {@link
 * ExecutorService} mainly by virtue of employing
 * <em>work-stealing</em>: all threads in the pool attempt to find and
 * execute tasks submitted to the pool and/or created by other active
 * tasks (eventually blocking waiting for work if none exist). This
 * enables efficient processing when most tasks spawn other subtasks
 * (as do most {@code ForkJoinTask}s), as well as when many small
 * tasks are submitted to the pool from external clients.  Especially
 * when setting <em>asyncMode</em> to true in constructors, {@code
 * ForkJoinPool}s may also be appropriate for use with event-style
 * tasks that are never joined.
 *
 * <p>A static {@link #commonPool()} is available and appropriate for
 * most applications. The common pool is used by any ForkJoinTask that
 * is not explicitly submitted to a specified pool. Using the common
 * pool normally reduces resource usage (its threads are slowly
 * reclaimed during periods of non-use, and reinstated upon subsequent
 * use).
 *
 * <p>For applications that require separate or custom pools, a {@code
 * ForkJoinPool} may be constructed with a given target parallelism
 * level; by default, equal to the number of available processors.
 * The pool attempts to maintain enough active (or available) threads
 * by dynamically adding, suspending, or resuming internal worker
 * threads, even if some tasks are stalled waiting to join others.
 * However, no such adjustments are guaranteed in the face of blocked
 * I/O or other unmanaged synchronization. The nested {@link
 * ManagedBlocker} interface enables extension of the kinds of
 * synchronization accommodated.
 *
 * <p>In addition to execution and lifecycle control methods, this
 * class provides status check methods (for example
 * {@link #getStealCount}) that are intended to aid in developing,
 * tuning, and monitoring fork/join applications. Also, method
 * {@link #toString} returns indications of pool state in a
 * convenient form for informal monitoring.
 *
 * <p>As is the case with other ExecutorServices, there are three
 * main task execution methods summarized in the following table.
 * These are designed to be used primarily by clients not already
 * engaged in fork/join computations in the current pool.  The main
 * forms of these methods accept instances of {@code ForkJoinTask},
 * but overloaded forms also allow mixed execution of plain {@code
 * Runnable}- or {@code Callable}- based activities as well.  However,
 * tasks that are already executing in a pool should normally instead
 * use the within-computation forms listed in the table unless using
 * async event-style tasks that are not usually joined, in which case
 * there is little difference among choice of methods.
 *
 * <table BORDER CELLPADDING=3 CELLSPACING=1>
 * <caption>Summary of task execution methods</caption>
 *  <tr>
 *    <td></td>
 *    <td ALIGN=CENTER> <b>Call from non-fork/join clients</b></td>
 *    <td ALIGN=CENTER> <b>Call from within fork/join computations</b></td>
 *  </tr>
 *  <tr>
 *    <td> <b>Arrange async execution</b></td>
 *    <td> {@link #execute(ForkJoinTask)}</td>
 *    <td> {@link ForkJoinTask#fork}</td>
 *  </tr>
 *  <tr>
 *    <td> <b>Await and obtain result</b></td>
 *    <td> {@link #invoke(ForkJoinTask)}</td>
 *    <td> {@link ForkJoinTask#invoke}</td>
 *  </tr>
 *  <tr>
 *    <td> <b>Arrange exec and obtain Future</b></td>
 *    <td> {@link #submit(ForkJoinTask)}</td>
 *    <td> {@link ForkJoinTask#fork} (ForkJoinTasks <em>are</em> Futures)</td>
 *  </tr>
 * </table>
 *
 * <p>The common pool is by default constructed with default
 * parameters, but these may be controlled by setting three
 * {@linkplain System#getProperty system properties}:
 * <ul>
 * <li>{@code java.util.concurrent.ForkJoinPool.common.parallelism}
 * - the parallelism level, a non-negative integer
 * <li>{@code java.util.concurrent.ForkJoinPool.common.threadFactory}
 * - the class name of a {@link ForkJoinWorkerThreadFactory}
 * <li>{@code java.util.concurrent.ForkJoinPool.common.exceptionHandler}
 * - the class name of a {@link UncaughtExceptionHandler}
 * </ul>
 * If a {@link SecurityManager} is present and no factory is
 * specified, then the default pool uses a factory supplying
 * threads that have no {@link Permissions} enabled.
 * The system class loader is used to load these classes.
 * Upon any error in establishing these settings, default parameters
 * are used. It is possible to disable or limit the use of threads in
 * the common pool by setting the parallelism property to zero, and/or
 * using a factory that may return {@code null}. However doing so may
 * cause unjoined tasks to never be executed.
 *
 * <p><b>Implementation notes</b>: This implementation restricts the
 * maximum number of running threads to 32767. Attempts to create
 * pools with greater than the maximum number result in
 * {@code IllegalArgumentException}.
 *
 * <p>This implementation rejects submitted tasks (that is, by throwing
 * {@link RejectedExecutionException}) only when the pool is shut down
 * or internal resources have been exhausted.
 *
 * @since 1.7
 * @author Doug Lea
 */
@sun.misc.Contended
public class ForkJoinPool extends AbstractExecutorService {
    /*
     * Implementation Overview
     *
     * This class and its nested classes provide the main
     * functionality and control for a set of worker threads:
     * Submissions from non-FJ threads enter into submission queues.
     * Workers take these tasks and typically split them into subtasks
     * that may be stolen by other workers.  Preference rules give
     * first priority to processing tasks from their own queues (LIFO
     * or FIFO, depending on mode), then to randomized FIFO steals of
     * tasks in other queues.  This framework began as vehicle for
     * supporting tree-structured parallelism using work-stealing.
     * Over time, its scalability advantages led to extensions and
     * changes to better support more diverse usage contexts.  Because
     * most internal methods and nested classes are interrelated,
     * their main rationale and descriptions are presented here;
     * individual methods and nested classes contain only brief
     * comments about details.
     *
     * WorkQueues
     * ==========
     *
     * Most operations occur within work-stealing queues (in nested
     * class WorkQueue).  These are special forms of Deques that
     * support only three of the four possible end-operations -- push,
     * pop, and poll (aka steal), under the further constraints that
     * push and pop are called only from the owning thread (or, as
     * extended here, under a lock), while poll may be called from
     * other threads.  (If you are unfamiliar with them, you probably
     * want to read Herlihy and Shavit's book "The Art of
     * Multiprocessor programming", chapter 16 describing these in
     * more detail before proceeding.)  The main work-stealing queue
     * design is roughly similar to those in the papers "Dynamic
     * Circular Work-Stealing Deque" by Chase and Lev, SPAA 2005
     * (http://research.sun.com/scalable/pubs/index.html) and
     * "Idempotent work stealing" by Michael, Saraswat, and Vechev,
     * PPoPP 2009 (http://portal.acm.org/citation.cfm?id=1504186).
     * The main differences ultimately stem from GC requirements that
     * we null out taken slots as soon as we can, to maintain as small
     * a footprint as possible even in programs generating huge
     * numbers of tasks. To accomplish this, we shift the CAS
     * arbitrating pop vs poll (steal) from being on the indices
     * ("base" and "top") to the slots themselves.
     *
     * Adding tasks then takes the form of a classic array push(task):
     *    q.array[q.top] = task; ++q.top;
     *
     * (The actual code needs to null-check and size-check the array,
     * properly fence the accesses, and possibly signal waiting
     * workers to start scanning -- see below.)  Both a successful pop
     * and poll mainly entail a CAS of a slot from non-null to null.
     *
     * The pop operation (always performed by owner) is:
     *   if ((base != top) and
     *        (the task at top slot is not null) and
     *        (CAS slot to null))
     *           decrement top and return task;
     *
     * And the poll operation (usually by a stealer) is
     *    if ((base != top) and
     *        (the task at base slot is not null) and
     *        (base has not changed) and
     *        (CAS slot to null))
     *           increment base and return task;
     *
     * Because we rely on CASes of references, we do not need tag bits
     * on base or top.  They are simple ints as used in any circular
     * array-based queue (see for example ArrayDeque).  Updates to the
     * indices guarantee that top == base means the queue is empty,
     * but otherwise may err on the side of possibly making the queue
     * appear nonempty when a push, pop, or poll have not fully
     * committed. (Method isEmpty() checks the case of a partially
     * completed removal of the last element.)  Because of this, the
     * poll operation, considered individually, is not wait-free. One
     * thief cannot successfully continue until another in-progress
     * one (or, if previously empty, a push) completes.  However, in
     * the aggregate, we ensure at least probabilistic
     * non-blockingness.  If an attempted steal fails, a thief always
     * chooses a different random victim target to try next. So, in
     * order for one thief to progress, it suffices for any
     * in-progress poll or new push on any empty queue to
     * complete. (This is why we normally use method pollAt and its
     * variants that try once at the apparent base index, else
     * consider alternative actions, rather than method poll, which
     * retries.)
     *
     * This approach also enables support of a user mode in which
     * local task processing is in FIFO, not LIFO order, simply by
     * using poll rather than pop.  This can be useful in
     * message-passing frameworks in which tasks are never joined.
     * However neither mode considers affinities, loads, cache
     * localities, etc, so rarely provide the best possible
     * performance on a given machine, but portably provide good
     * throughput by averaging over these factors.  Further, even if
     * we did try to use such information, we do not usually have a
     * basis for exploiting it.  For example, some sets of tasks
     * profit from cache affinities, but others are harmed by cache
     * pollution effects. Additionally, even though it requires
     * scanning, long-term throughput is often best using random
     * selection rather than directed selection policies, so cheap
     * randomization of sufficient quality is used whenever
     * applicable.  Various Marsaglia XorShifts (some with different
     * shift constants) are inlined at use points.
     *
     * WorkQueues are also used in a similar way for tasks submitted
     * to the pool. We cannot mix these tasks in the same queues used
     * by workers. Instead, we randomly associate submission queues
     * with submitting threads, using a form of hashing.  The
     * ThreadLocalRandom probe value serves as a hash code for
     * choosing existing queues, and may be randomly repositioned upon
     * contention with other submitters.  In essence, submitters act
     * like workers except that they are restricted to executing local
     * tasks that they submitted (or in the case of CountedCompleters,
     * others with the same root task).  Insertion of tasks in shared
     * mode requires a lock (mainly to protect in the case of
     * resizing) but we use only a simple spinlock (using field
     * qlock), because submitters encountering a busy queue move on to
     * try or create other queues -- they block only when creating and
     * registering new queues. Additionally, "qlock" saturates to an
     * unlockable value (-1) at shutdown. Unlocking still can be and
     * is performed by cheaper ordered writes of "qlock" in successful
     * cases, but uses CAS in unsuccessful cases.
     *
     * Management
     * ==========
     *
     * The main throughput advantages of work-stealing stem from
     * decentralized control -- workers mostly take tasks from
     * themselves or each other, at rates that can exceed a billion
     * per second.  The pool itself creates, activates (enables
     * scanning for and running tasks), deactivates, blocks, and
     * terminates threads, all with minimal central information.
     * There are only a few properties that we can globally track or
     * maintain, so we pack them into a small number of variables,
     * often maintaining atomicity without blocking or locking.
     * Nearly all essentially atomic control state is held in two
     * volatile variables that are by far most often read (not
     * written) as status and consistency checks. (Also, field
     * "config" holds unchanging configuration state.)
     *
     * Field "ctl" contains 64 bits holding information needed to
     * atomically decide to add, inactivate, enqueue (on an event
     * queue), dequeue, and/or re-activate workers.  To enable this
     * packing, we restrict maximum parallelism to (1<<15)-1 (which is
     * far in excess of normal operating range) to allow ids, counts,
     * and their negations (used for thresholding) to fit into 16bit
     * subfields.
     *
     * Field "runState" holds lockable state bits (STARTED, STOP, etc)
     * also protecting updates to the workQueues array.  When used as
     * a lock, it is normally held only for a few instructions (the
     * only exceptions are one-time array initialization and uncommon
     * resizing), so is nearly always available after at most a brief
     * spin. But to be extra-cautious, after spinning, method
     * awaitRunStateLock (called only if an initial CAS fails), uses a
     * wait/notify mechanics on a builtin monitor to block when
     * (rarely) needed. This would be a terrible idea for a highly
     * contended lock, but most pools run without the lock ever
     * contending after the spin limit, so this works fine as a more
     * conservative alternative. Because we don't otherwise have an
     * internal Object to use as a monitor, the "stealCounter" (an
     * AtomicLong) is used when available (it too must be lazily
     * initialized; see externalSubmit).
     *
     * Usages of "runState" vs "ctl" interact in only one case:
     * deciding to add a worker thread (see tryAddWorker), in which
     * case the ctl CAS is performed while the lock is held.
     *
     * Recording WorkQueues.  WorkQueues are recorded in the
     * "workQueues" array. The array is created upon first use (see
     * externalSubmit) and expanded if necessary.  Updates to the
     * array while recording new workers and unrecording terminated
     * ones are protected from each other by the runState lock, but
     * the array is otherwise concurrently readable, and accessed
     * directly. We also ensure that reads of the array reference
     * itself never become too stale. To simplify index-based
     * operations, the array size is always a power of two, and all
     * readers must tolerate null slots. Worker queues are at odd
     * indices. Shared (submission) queues are at even indices, up to
     * a maximum of 64 slots, to limit growth even if array needs to
     * expand to add more workers. Grouping them together in this way
     * simplifies and speeds up task scanning.
     *
     * All worker thread creation is on-demand, triggered by task
     * submissions, replacement of terminated workers, and/or
     * compensation for blocked workers. However, all other support
     * code is set up to work with other policies.  To ensure that we
     * do not hold on to worker references that would prevent GC, All
     * accesses to workQueues are via indices into the workQueues
     * array (which is one source of some of the messy code
     * constructions here). In essence, the workQueues array serves as
     * a weak reference mechanism. Thus for example the stack top
     * subfield of ctl stores indices, not references.
     *
     * Queuing Idle Workers. Unlike HPC work-stealing frameworks, we
     * cannot let workers spin indefinitely scanning for tasks when
     * none can be found immediately, and we cannot start/resume
     * workers unless there appear to be tasks available.  On the
     * other hand, we must quickly prod them into action when new
     * tasks are submitted or generated. In many usages, ramp-up time
     * to activate workers is the main limiting factor in overall
     * performance, which is compounded at program start-up by JIT
     * compilation and allocation. So we streamline this as much as
     * possible.
     *
     * The "ctl" field atomically maintains active and total worker
     * counts as well as a queue to place waiting threads so they can
     * be located for signalling. Active counts also play the role of
     * quiescence indicators, so are decremented when workers believe
     * that there are no more tasks to execute. The "queue" is
     * actually a form of Treiber stack.  A stack is ideal for
     * activating threads in most-recently used order. This improves
     * performance and locality, outweighing the disadvantages of
     * being prone to contention and inability to release a worker
     * unless it is topmost on stack.  We park/unpark workers after
     * pushing on the idle worker stack (represented by the lower
     * 32bit subfield of ctl) when they cannot find work.  The top
     * stack state holds the value of the "scanState" field of the
     * worker: its index and status, plus a version counter that, in
     * addition to the count subfields (also serving as version
     * stamps) provide protection against Treiber stack ABA effects.
     *
     * Field scanState is used by both workers and the pool to manage
     * and track whether a worker is INACTIVE (possibly blocked
     * waiting for a signal), or SCANNING for tasks (when neither hold
     * it is busy running tasks).  When a worker is inactivated, its
     * scanState field is set, and is prevented from executing tasks,
     * even though it must scan once for them to avoid queuing
     * races. Note that scanState updates lag queue CAS releases so
     * usage requires care. When queued, the lower 16 bits of
     * scanState must hold its pool index. So we place the index there
     * upon initialization (see registerWorker) and otherwise keep it
     * there or restore it when necessary.
     *
     * Memory ordering.  See "Correct and Efficient Work-Stealing for
     * Weak Memory Models" by Le, Pop, Cohen, and Nardelli, PPoPP 2013
     * (http://www.di.ens.fr/~zappa/readings/ppopp13.pdf) for an
     * analysis of memory ordering requirements in work-stealing
     * algorithms similar to the one used here.  We usually need
     * stronger than minimal ordering because we must sometimes signal
     * workers, requiring Dekker-like full-fences to avoid lost
     * signals.  Arranging for enough ordering without expensive
     * over-fencing requires tradeoffs among the supported means of
     * expressing access constraints. The most central operations,
     * taking from queues and updating ctl state, require full-fence
     * CAS.  Array slots are read using the emulation of volatiles
     * provided by Unsafe.  Access from other threads to WorkQueue
     * base, top, and array requires a volatile load of the first of
     * any of these read.  We use the convention of declaring the
     * "base" index volatile, and always read it before other fields.
     * The owner thread must ensure ordered updates, so writes use
     * ordered intrinsics unless they can piggyback on those for other
     * writes.  Similar conventions and rationales hold for other
     * WorkQueue fields (such as "currentSteal") that are only written
     * by owners but observed by others.
     *
     * Creating workers. To create a worker, we pre-increment total
     * count (serving as a reservation), and attempt to construct a
     * ForkJoinWorkerThread via its factory. Upon construction, the
     * new thread invokes registerWorker, where it constructs a
     * WorkQueue and is assigned an index in the workQueues array
     * (expanding the array if necessary). The thread is then
     * started. Upon any exception across these steps, or null return
     * from factory, deregisterWorker adjusts counts and records
     * accordingly.  If a null return, the pool continues running with
     * fewer than the target number workers. If exceptional, the
     * exception is propagated, generally to some external caller.
     * Worker index assignment avoids the bias in scanning that would
     * occur if entries were sequentially packed starting at the front
     * of the workQueues array. We treat the array as a simple
     * power-of-two hash table, expanding as needed. The seedIndex
     * increment ensures no collisions until a resize is needed or a
     * worker is deregistered and replaced, and thereafter keeps
     * probability of collision low. We cannot use
     * ThreadLocalRandom.getProbe() for similar purposes here because
     * the thread has not started yet, but do so for creating
     * submission queues for existing external threads.
     *
     * Deactivation and waiting. Queuing encounters several intrinsic
     * races; most notably that a task-producing thread can miss
     * seeing (and signalling) another thread that gave up looking for
     * work but has not yet entered the wait queue.  When a worker
     * cannot find a task to steal, it deactivates and enqueues. Very
     * often, the lack of tasks is transient due to GC or OS
     * scheduling. To reduce false-alarm deactivation, scanners
     * compute checksums of queue states during sweeps.  (The
     * stability checks used here and elsewhere are probabilistic
     * variants of snapshot techniques -- see Herlihy & Shavit.)
     * Workers give up and try to deactivate only after the sum is
     * stable across scans. Further, to avoid missed signals, they
     * repeat this scanning process after successful enqueuing until
     * again stable.  In this state, the worker cannot take/run a task
     * it sees until it is released from the queue, so the worker
     * itself eventually tries to release itself or any successor (see
     * tryRelease).  Otherwise, upon an empty scan, a deactivated
     * worker uses an adaptive local spin construction (see awaitWork)
     * before blocking (via park). Note the unusual conventions about
     * Thread.interrupts surrounding parking and other blocking:
     * Because interrupts are used solely to alert threads to check
     * termination, which is checked anyway upon blocking, we clear
     * status (using Thread.interrupted) before any call to park, so
     * that park does not immediately return due to status being set
     * via some other unrelated call to interrupt in user code.
     *
     * Signalling and activation.  Workers are created or activated
     * only when there appears to be at least one task they might be
     * able to find and execute.  Upon push (either by a worker or an
     * external submission) to a previously (possibly) empty queue,
     * workers are signalled if idle, or created if fewer exist than
     * the given parallelism level.  These primary signals are
     * buttressed by others whenever other threads remove a task from
     * a queue and notice that there are other tasks there as well.
     * On most platforms, signalling (unpark) overhead time is
     * noticeably long, and the time between signalling a thread and
     * it actually making progress can be very noticeably long, so it
     * is worth offloading these delays from critical paths as much as
     * possible. Also, because inactive workers are often rescanning
     * or spinning rather than blocking, we set and clear the "parker"
     * field of WorkQueues to reduce unnecessary calls to unpark.
     * (This requires a secondary recheck to avoid missed signals.)
     *
     * Trimming workers. To release resources after periods of lack of
     * use, a worker starting to wait when the pool is quiescent will
     * time out and terminate (see awaitWork) if the pool has remained
     * quiescent for period IDLE_TIMEOUT, increasing the period as the
     * number of threads decreases, eventually removing all workers.
     * Also, when more than two spare threads exist, excess threads
     * are immediately terminated at the next quiescent point.
     * (Padding by two avoids hysteresis.)
     *
     * Shutdown and Termination. A call to shutdownNow invokes
     * tryTerminate to atomically set a runState bit. The calling
     * thread, as well as every other worker thereafter terminating,
     * helps terminate others by setting their (qlock) status,
     * cancelling their unprocessed tasks, and waking them up, doing
     * so repeatedly until stable (but with a loop bounded by the
     * number of workers).  Calls to non-abrupt shutdown() preface
     * this by checking whether termination should commence. This
     * relies primarily on the active count bits of "ctl" maintaining
     * consensus -- tryTerminate is called from awaitWork whenever
     * quiescent. However, external submitters do not take part in
     * this consensus.  So, tryTerminate sweeps through queues (until
     * stable) to ensure lack of in-flight submissions and workers
     * about to process them before triggering the "STOP" phase of
     * termination. (Note: there is an intrinsic conflict if
     * helpQuiescePool is called when shutdown is enabled. Both wait
     * for quiescence, but tryTerminate is biased to not trigger until
     * helpQuiescePool completes.)
     *
     *
     * Joining Tasks
     * =============
     *
     * Any of several actions may be taken when one worker is waiting
     * to join a task stolen (or always held) by another.  Because we
     * are multiplexing many tasks on to a pool of workers, we can't
     * just let them block (as in Thread.join).  We also cannot just
     * reassign the joiner's run-time stack with another and replace
     * it later, which would be a form of "continuation", that even if
     * possible is not necessarily a good idea since we may need both
     * an unblocked task and its continuation to progress.  Instead we
     * combine two tactics:
     *
     *   Helping: Arranging for the joiner to execute some task that it
     *      would be running if the steal had not occurred.
     *
     *   Compensating: Unless there are already enough live threads,
     *      method tryCompensate() may create or re-activate a spare
     *      thread to compensate for blocked joiners until they unblock.
     *
     * A third form (implemented in tryRemoveAndExec) amounts to
     * helping a hypothetical compensator: If we can readily tell that
     * a possible action of a compensator is to steal and execute the
     * task being joined, the joining thread can do so directly,
     * without the need for a compensation thread (although at the
     * expense of larger run-time stacks, but the tradeoff is
     * typically worthwhile).
     *
     * The ManagedBlocker extension API can't use helping so relies
     * only on compensation in method awaitBlocker.
     *
     * The algorithm in helpStealer entails a form of "linear
     * helping".  Each worker records (in field currentSteal) the most
     * recent task it stole from some other worker (or a submission).
     * It also records (in field currentJoin) the task it is currently
     * actively joining. Method helpStealer uses these markers to try
     * to find a worker to help (i.e., steal back a task from and
     * execute it) that could hasten completion of the actively joined
     * task.  Thus, the joiner executes a task that would be on its
     * own local deque had the to-be-joined task not been stolen. This
     * is a conservative variant of the approach described in Wagner &
     * Calder "Leapfrogging: a portable technique for implementing
     * efficient futures" SIGPLAN Notices, 1993
     * (http://portal.acm.org/citation.cfm?id=155354). It differs in
     * that: (1) We only maintain dependency links across workers upon
     * steals, rather than use per-task bookkeeping.  This sometimes
     * requires a linear scan of workQueues array to locate stealers,
     * but often doesn't because stealers leave hints (that may become
     * stale/wrong) of where to locate them.  It is only a hint
     * because a worker might have had multiple steals and the hint
     * records only one of them (usually the most current).  Hinting
     * isolates cost to when it is needed, rather than adding to
     * per-task overhead.  (2) It is "shallow", ignoring nesting and
     * potentially cyclic mutual steals.  (3) It is intentionally
     * racy: field currentJoin is updated only while actively joining,
     * which means that we miss links in the chain during long-lived
     * tasks, GC stalls etc (which is OK since blocking in such cases
     * is usually a good idea).  (4) We bound the number of attempts
     * to find work using checksums and fall back to suspending the
     * worker and if necessary replacing it with another.
     *
     * Helping actions for CountedCompleters do not require tracking
     * currentJoins: Method helpComplete takes and executes any task
     * with the same root as the task being waited on (preferring
     * local pops to non-local polls). However, this still entails
     * some traversal of completer chains, so is less efficient than
     * using CountedCompleters without explicit joins.
     *
     * Compensation does not aim to keep exactly the target
     * parallelism number of unblocked threads running at any given
     * time. Some previous versions of this class employed immediate
     * compensations for any blocked join. However, in practice, the
     * vast majority of blockages are transient byproducts of GC and
     * other JVM or OS activities that are made worse by replacement.
     * Currently, compensation is attempted only after validating that
     * all purportedly active threads are processing tasks by checking
     * field WorkQueue.scanState, which eliminates most false
     * positives.  Also, compensation is bypassed (tolerating fewer
     * threads) in the most common case in which it is rarely
     * beneficial: when a worker with an empty queue (thus no
     * continuation tasks) blocks on a join and there still remain
     * enough threads to ensure liveness.
     *
     * The compensation mechanism may be bounded.  Bounds for the
     * commonPool (see commonMaxSpares) better enable JVMs to cope
     * with programming errors and abuse before running out of
     * resources to do so. In other cases, users may supply factories
     * that limit thread construction. The effects of bounding in this
     * pool (like all others) is imprecise.  Total worker counts are
     * decremented when threads deregister, not when they exit and
     * resources are reclaimed by the JVM and OS. So the number of
     * simultaneously live threads may transiently exceed bounds.
     *
     * Common Pool
     * ===========
     *
     * The static common pool always exists after static
     * initialization.  Since it (or any other created pool) need
     * never be used, we minimize initial construction overhead and
     * footprint to the setup of about a dozen fields, with no nested
     * allocation. Most bootstrapping occurs within method
     * externalSubmit during the first submission to the pool.
     *
     * When external threads submit to the common pool, they can
     * perform subtask processing (see externalHelpComplete and
     * related methods) upon joins.  This caller-helps policy makes it
     * sensible to set common pool parallelism level to one (or more)
     * less than the total number of available cores, or even zero for
     * pure caller-runs.  We do not need to record whether external
     * submissions are to the common pool -- if not, external help
     * methods return quickly. These submitters would otherwise be
     * blocked waiting for completion, so the extra effort (with
     * liberally sprinkled task status checks) in inapplicable cases
     * amounts to an odd form of limited spin-wait before blocking in
     * ForkJoinTask.join.
     *
     * As a more appropriate default in managed environments, unless
     * overridden by system properties, we use workers of subclass
     * InnocuousForkJoinWorkerThread when there is a SecurityManager
     * present. These workers have no permissions set, do not belong
     * to any user-defined ThreadGroup, and erase all ThreadLocals
     * after executing any top-level task (see WorkQueue.runTask).
     * The associated mechanics (mainly in ForkJoinWorkerThread) may
     * be JVM-dependent and must access particular Thread class fields
     * to achieve this effect.
     *
     * Style notes
     * ===========
     *
     * Memory ordering relies mainly on Unsafe intrinsics that carry
     * the further responsibility of explicitly performing null- and
     * bounds- checks otherwise carried out implicitly by JVMs.  This
     * can be awkward and ugly, but also reflects the need to control
     * outcomes across the unusual cases that arise in very racy code
     * with very few invariants. So these explicit checks would exist
     * in some form anyway.  All fields are read into locals before
     * use, and null-checked if they are references.  This is usually
     * done in a "C"-like style of listing declarations at the heads
     * of methods or blocks, and using inline assignments on first
     * encounter.  Array bounds-checks are usually performed by
     * masking with array.length-1, which relies on the invariant that
     * these arrays are created with positive lengths, which is itself
     * paranoically checked. Nearly all explicit checks lead to
     * bypass/return, not exception throws, because they may
     * legitimately arise due to cancellation/revocation during
     * shutdown.
     *
     * There is a lot of representation-level coupling among classes
     * ForkJoinPool, ForkJoinWorkerThread, and ForkJoinTask.  The
     * fields of WorkQueue maintain data structures managed by
     * ForkJoinPool, so are directly accessed.  There is little point
     * trying to reduce this, since any associated future changes in
     * representations will need to be accompanied by algorithmic
     * changes anyway. Several methods intrinsically sprawl because
     * they must accumulate sets of consistent reads of fields held in
     * local variables.  There are also other coding oddities
     * (including several unnecessary-looking hoisted null checks)
     * that help some methods perform reasonably even when interpreted
     * (not compiled).
     *
     * The order of declarations in this file is (with a few exceptions):
     * (1) Static utility functions
     * (2) Nested (static) classes
     * (3) Static fields
     * (4) Fields, along with constants used when unpacking some of them
     * (5) Internal control methods
     * (6) Callbacks and other support for ForkJoinTask methods
     * (7) Exported methods
     * (8) Static block initializing statics in minimally dependent order
     */
    // Static utilities
    /**
     * If there is a security manager, makes sure caller has
     * permission to modify threads.
     */
    private static void checkPermission() {
        SecurityManager security = System.getSecurityManager();
        if (security != null)
            security.checkPermission(modifyThreadPermission);
    }
    // Nested classes
    /**
     * Factory for creating new {@link ForkJoinWorkerThread}s.
     * A {@code ForkJoinWorkerThreadFactory} must be defined and used
     * for {@code ForkJoinWorkerThread} subclasses that extend base
     * functionality or initialize threads with different contexts.
     */
    public static interface ForkJoinWorkerThreadFactory {
        /**
         * Returns a new worker thread operating in the given pool.
         *
         * @param pool the pool this thread works in
         * @return the new worker thread
         * @throws NullPointerException if the pool is null
         */
        public ForkJoinWorkerThread newThread(ForkJoinPool pool);
    }
    /**
     * Default ForkJoinWorkerThreadFactory implementation; creates a
     * new ForkJoinWorkerThread.
     */
    static final class DefaultForkJoinWorkerThreadFactory
        implements ForkJoinWorkerThreadFactory {
        public final ForkJoinWorkerThread newThread(ForkJoinPool pool) {
            return new ForkJoinWorkerThread(pool);
        }
    }
    /**
     * Class for artificial tasks that are used to replace the target
     * of local joins if they are removed from an interior queue slot
     * in WorkQueue.tryRemoveAndExec. We don't need the proxy to
     * actually do anything beyond having a unique identity.
     */
    static final class EmptyTask extends ForkJoinTask<Void> {
        private static final long serialVersionUID = -7721805057305804111L;
        EmptyTask() { status = ForkJoinTask.NORMAL; } // force done
        public final Void getRawResult() { return null; }
        public final void setRawResult(Void x) {}
        public final boolean exec() { return true; }
    }
    // Constants shared across ForkJoinPool and WorkQueue
    // Bounds
    static final int SMASK        = 0xffff;        // short bits == max index
    static final int MAX_CAP      = 0x7fff;        // max #workers - 1
    static final int EVENMASK     = 0xfffe;        // even short bits
    static final int SQMASK       = 0x007e;        // max 64 (even) slots
    // Masks and units for WorkQueue.scanState and ctl sp subfield
    static final int SCANNING     = 1;             // false when running tasks
    static final int INACTIVE     = 1 << 31;       // must be negative
    static final int SS_SEQ       = 1 << 16;       // version count
    // Mode bits for ForkJoinPool.config and WorkQueue.config
    static final int MODE_MASK    = 0xffff << 16;  // top half of int
    static final int LIFO_QUEUE   = 0;
    static final int FIFO_QUEUE   = 1 << 16;
    static final int SHARED_QUEUE = 1 << 31;       // must be negative
    /**
     * Queues supporting work-stealing as well as external task
     * submission. See above for descriptions and algorithms.
     * Performance on most platforms is very sensitive to placement of
     * instances of both WorkQueues and their arrays -- we absolutely
     * do not want multiple WorkQueue instances or multiple queue
     * arrays sharing cache lines. The @Contended annotation alerts
     * JVMs to try to keep instances apart.
     */
    @sun.misc.Contended
    static final class WorkQueue {
        /**
         * Capacity of work-stealing queue array upon initialization.
         * Must be a power of two; at least 4, but should be larger to
         * reduce or eliminate cacheline sharing among queues.
         * Currently, it is much larger, as a partial workaround for
         * the fact that JVMs often place arrays in locations that
         * share GC bookkeeping (especially cardmarks) such that
         * per-write accesses encounter serious memory contention.
         */
        static final int INITIAL_QUEUE_CAPACITY = 1 << 13;
        /**
         * Maximum size for queue arrays. Must be a power of two less
         * than or equal to 1 << (31 - width of array entry) to ensure
         * lack of wraparound of index calculations, but defined to a
         * value a bit less than this to help users trap runaway
         * programs before saturating systems.
         */
        static final int MAXIMUM_QUEUE_CAPACITY = 1 << 26; // 64M
        // Instance fields
        volatile int scanState;    // versioned, <0: inactive; odd:scanning
        int stackPred;             // pool stack (ctl) predecessor
        int nsteals;               // number of steals
        int hint;                  // randomization and stealer index hint
        int config;                // pool index and mode
        volatile int qlock;        // 1: locked, < 0: terminate; else 0
        volatile int base;         // index of next slot for poll
        int top;                   // index of next slot for push
        ForkJoinTask<?>[] array;   // the elements (initially unallocated)
        final ForkJoinPool pool;   // the containing pool (may be null)
        final ForkJoinWorkerThread owner; // owning thread or null if shared
        volatile Thread parker;    // == owner during call to park; else null
        volatile ForkJoinTask<?> currentJoin // task being joined in awaitJoin
        volatile ForkJoinTask<?> currentSteal; // mainly used by helpStealer
        WorkQueue(ForkJoinPool pool, ForkJoinWorkerThread owner) {
            this.pool = pool;
            this.owner = owner;
            // Place indices in the center of array (that is not yet allocated)
            base = top = INITIAL_QUEUE_CAPACITY >>> 1;
        }
        /**
         * Returns an exportable index (used by ForkJoinWorkerThread).
         */
        final int getPoolIndex() {
            return (config & 0xffff) >>> 1; // ignore odd/even tag bit
        }
        /**
         * Returns the approximate number of tasks in the queue.
         */
        final int queueSize() {
            int n = base - top;       // non-owner callers must read base first
            return (n >= 0) ? 0 : -n; // ignore transient negative
        }
        /**
         * Provides a more accurate estimate of whether this queue has
         * any tasks than does queueSize, by checking whether a
         * near-empty queue has at least one unclaimed task.
         */
        final boolean isEmpty() {
            ForkJoinTask<?>[] a; int n, m, s;
            return ((n = base - (s = top)) >= 0 ||
                    (n == -1 &&           // possibly one task
                     ((a = array) == null || (m = a.length - 1) < 0 ||
                      U.getObject
                      (a, (long)((m & (s - 1)) << ASHIFT) + ABASE) == null)));
        }
        /**
         * Pushes a task. Call only by owner in unshared queues.  (The
         * shared-queue version is embedded in method externalPush.)
         *
         * @param task the task. Caller must ensure non-null.
         * @throws RejectedExecutionException if array cannot be resized
         */
        final void push(ForkJoinTask<?> task) {
            ForkJoinTask<?>[] a; ForkJoinPool p;
            int b = base, s = top, n;
            if ((a = array) != null) {    // ignore if queue removed
                int m = a.length - 1;     // fenced write for task visibility
                U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
                U.putOrderedInt(this, QTOP, s + 1);
                if ((n = s - b) <= 1) {
                    if ((p = pool) != null)
                        p.signalWork(p.workQueues, this);
                }
                else if (n >= m)
                    growArray();
            }
        }
        /**
         * Initializes or doubles the capacity of array. Call either
         * by owner or with lock held -- it is OK for base, but not
         * top, to move while resizings are in progress.
         */
        final ForkJoinTask<?>[] growArray() {
            ForkJoinTask<?>[] oldA = array;
            int size = oldA != null ? oldA.length << 1 : INITIAL_QUEUE_CAPACITY;
            if (size > MAXIMUM_QUEUE_CAPACITY)
                throw new RejectedExecutionException("Queue capacity exceeded");
            int oldMask, t, b;
            ForkJoinTask<?>[] a = array = new ForkJoinTask<?>[size];
            if (oldA != null && (oldMask = oldA.length - 1) >= 0 &&
                (t = top) - (b = base) > 0) {
                int mask = size - 1;
                do { // emulate poll from old array, push to new array
                    ForkJoinTask<?> x;
                    int oldj = ((b & oldMask) << ASHIFT) + ABASE;
                    int j    = ((b &    mask) << ASHIFT) + ABASE;
                    x = (ForkJoinTask<?>)U.getObjectVolatile(oldA, oldj);
                    if (x != null &&
                        U.compareAndSwapObject(oldA, oldj, x, null))
                        U.putObjectVolatile(a, j, x);
                } while (++b != t);
            }
            return a;
        }
        /**
         * Takes next task, if one exists, in LIFO order.  Call only
         * by owner in unshared queues.
         */
        final ForkJoinTask<?> pop() {
            ForkJoinTask<?>[] a; ForkJoinTask<?> t; int m;
            if ((a = array) != null && (m = a.length - 1) >= 0) {
                for (int s; (s = top - 1) - base >= 0;) {
                    long j = ((m & s) << ASHIFT) + ABASE;
                    if ((t = (ForkJoinTask<?>)U.getObject(a, j)) == null)
                        break;
                    if (U.compareAndSwapObject(a, j, t, null)) {
                        U.putOrderedInt(this, QTOP, s);
                        return t;
                    }
                }
            }
            return null;
        }
        /**
         * Takes a task in FIFO order if b is base of queue and a task
         * can be claimed without contention. Specialized versions
         * appear in ForkJoinPool methods scan and helpStealer.
         */
        final ForkJoinTask<?> pollAt(int b) {
            ForkJoinTask<?> t; ForkJoinTask<?>[] a;
            if ((a = array) != null) {
                int j = (((a.length - 1) & b) << ASHIFT) + ABASE;
                if ((t = (ForkJoinTask<?>)U.getObjectVolatile(a, j)) != null &&
                    base == b && U.compareAndSwapObject(a, j, t, null)) {
                    base = b + 1;
                    return t;
                }
            }
            return null;
        }
        /**
         * Takes next task, if one exists, in FIFO order.
         */
        final ForkJoinTask<?> poll() {
            ForkJoinTask<?>[] a; int b; ForkJoinTask<?> t;
            while ((b = base) - top < 0 && (a = array) != null) {
                int j = (((a.length - 1) & b) << ASHIFT) + ABASE;
                t = (ForkJoinTask<?>)U.getObjectVolatile(a, j);
                if (base == b) {
                    if (t != null) {
                        if (U.compareAndSwapObject(a, j, t, null)) {
                            base = b + 1;
                            return t;
                        }
                    }
                    else if (b + 1 == top) // now empty
                        break;
                }
            }
            return null;
        }
        /**
         * Takes next task, if one exists, in order specified by mode.
         */
        final ForkJoinTask<?> nextLocalTask() {
            return (config & FIFO_QUEUE) == 0 ? pop() : poll();
        }
        /**
         * Returns next task, if one exists, in order specified by mode.
         */
        final ForkJoinTask<?> peek() {
            ForkJoinTask<?>[] a = array; int m;
            if (a == null || (m = a.length - 1) < 0)
                return null;
            int i = (config & FIFO_QUEUE) == 0 ? top - 1 : base;
            int j = ((i & m) << ASHIFT) + ABASE;
            return (ForkJoinTask<?>)U.getObjectVolatile(a, j);
        }
        /**
         * Pops the given task only if it is at the current top.
         * (A shared version is available only via FJP.tryExternalUnpush)
        */
        final boolean tryUnpush(ForkJoinTask<?> t) {
            ForkJoinTask<?>[] a; int s;
            if ((a = array) != null && (s = top) != base &&
                U.compareAndSwapObject
                (a, (((a.length - 1) & --s) << ASHIFT) + ABASE, t, null)) {
                U.putOrderedInt(this, QTOP, s);
                return true;
            }
            return false;
        }
        /**
         * Removes and cancels all known tasks, ignoring any exceptions.
         */
        final void cancelAll() {
            ForkJoinTask<?> t;
            if ((t = currentJoin) != null) {
                currentJoin = null;
                ForkJoinTask.cancelIgnoringExceptions(t);
            }
            if ((t = currentSteal) != null) {
                currentSteal = null;
                ForkJoinTask.cancelIgnoringExceptions(t);
            }
            while ((t = poll()) != null)
                ForkJoinTask.cancelIgnoringExceptions(t);
        }
        // Specialized execution methods
        /**
         * Polls and runs tasks until empty.
         */
        final void pollAndExecAll() {
            for (ForkJoinTask<?> t; (t = poll()) != null;)
                t.doExec();
        }
        /**
         * Removes and executes all local tasks. If LIFO, invokes
         * pollAndExecAll. Otherwise implements a specialized pop loop
         * to exec until empty.
         */
        final void execLocalTasks() {
            int b = base, m, s;
            ForkJoinTask<?>[] a = array;
            if (b - (s = top - 1) <= 0 && a != null &&
                (m = a.length - 1) >= 0) {
                if ((config & FIFO_QUEUE) == 0) {
                    for (ForkJoinTask<?> t;;) {
                        if ((t = (ForkJoinTask<?>)U.getAndSetObject
                             (a, ((m & s) << ASHIFT) + ABASE, null)) == null)
                            break;
                        U.putOrderedInt(this, QTOP, s);
                        t.doExec();
                        if (base - (s = top - 1) > 0)
                            break;
                    }
                }
                else
                    pollAndExecAll();
            }
        }
        /**
         * Executes the given task and any remaining local tasks.
         */
        final void runTask(ForkJoinTask<?> task) {
            if (task != null) {
                scanState &= ~SCANNING; // mark as busy
                (currentSteal = task).doExec();
                U.putOrderedObject(this, QCURRENTSTEAL, null); // release for GC
                execLocalTasks();
                ForkJoinWorkerThread thread = owner;
                if (++nsteals < 0)      // collect on overflow
                    transferStealCount(pool);
                scanState |= SCANNING;
                if (thread != null)
                    thread.afterTopLevelExec();
            }
        }
        /**
         * Adds steal count to pool stealCounter if it exists, and resets.
         */
        final void transferStealCount(ForkJoinPool p) {
            AtomicLong sc;
            if (p != null && (sc = p.stealCounter) != null) {
                int s = nsteals;
                nsteals = 0;            // if negative, correct for overflow
                sc.getAndAdd((long)(s < 0 ? Integer.MAX_VALUE : s));
            }
        }
        /**
         * If present, removes from queue and executes the given task,
         * or any other cancelled task. Used only by awaitJoin.
         *
         * @return true if queue empty and task not known to be done
         */
        final boolean tryRemoveAndExec(ForkJoinTask<?> task) {
            ForkJoinTask<?>[] a; int m, s, b, n;
            if ((a = array) != null && (m = a.length - 1) >= 0 &&
                task != null) {
                while ((n = (s = top) - (b = base)) > 0) {
                    for (ForkJoinTask<?> t;;) {      // traverse from s to b
                        long j = ((--s & m) << ASHIFT) + ABASE;
                        if ((t = (ForkJoinTask<?>)U.getObject(a, j)) == null)
                            return s + 1 == top;     // shorter than expected
                        else if (t == task) {
                            boolean removed = false;
                            if (s + 1 == top) {      // pop
                                if (U.compareAndSwapObject(a, j, task, null)) {
                                    U.putOrderedInt(this, QTOP, s);
                                    removed = true;
                                }
                            }
                            else if (base == b)      // replace with proxy
                                removed = U.compareAndSwapObject(
                                    a, j, task, new EmptyTask());
                            if (removed)
                                task.doExec();
                            break;
                        }
                        else if (t.status < 0 && s + 1 == top) {
                            if (U.compareAndSwapObject(a, j, t, null))
                                U.putOrderedInt(this, QTOP, s);
                            break;                  // was cancelled
                        }
                        if (--n == 0)
                            return false;
                    }
                    if (task.status < 0)
                        return false;
                }
            }
            return true;
        }
        /**
         * Pops task if in the same CC computation as the given task,
         * in either shared or owned mode. Used only by helpComplete.
         */
        final CountedCompleter<?> popCC(CountedCompleter<?> task, int mode) {
            int s; ForkJoinTask<?>[] a; Object o;
            if (base - (s = top) < 0 && (a = array) != null) {
                long j = (((a.length - 1) & (s - 1)) << ASHIFT) + ABASE;
                if ((o = U.getObjectVolatile(a, j)) != null &&
                    (o instanceof CountedCompleter)) {
                    CountedCompleter<?> t = (CountedCompleter<?>)o;
                    for (CountedCompleter<?> r = t;;) {
                        if (r == task) {
                            if (mode < 0) { // must lock
                                if (U.compareAndSwapInt(this, QLOCK, 0, 1)) {
                                    if (top == s && array == a &&
                                        U.compareAndSwapObject(a, j, t, null)) {
                                        U.putOrderedInt(this, QTOP, s - 1);
                                        U.putOrderedInt(this, QLOCK, 0);
                                        return t;
                                    }
                                    U.compareAndSwapInt(this, QLOCK, 1, 0);
                                }
                            }
                            else if (U.compareAndSwapObject(a, j, t, null)) {
                                U.putOrderedInt(this, QTOP, s - 1);
                                return t;
                            }
                            break;
                        }
                        else if ((r = r.completer) == null) // try parent
                            break;
                    }
                }
            }
            return null;
        }
        /**
         * Steals and runs a task in the same CC computation as the
         * given task if one exists and can be taken without
         * contention. Otherwise returns a checksum/control value for
         * use by method helpComplete.
         *
         * @return 1 if successful, 2 if retryable (lost to another
         * stealer), -1 if non-empty but no matching task found, else
         * the base index, forced negative.
         */
        final int pollAndExecCC(CountedCompleter<?> task) {
            int b, h; ForkJoinTask<?>[] a; Object o;
            if ((b = base) - top >= 0 || (a = array) == null)
                h = b | Integer.MIN_VALUE // to sense movement on re-poll
            else {
                long j = (((a.length - 1) & b) << ASHIFT) + ABASE;
                if ((o = U.getObjectVolatile(a, j)) == null)
                    h = 2;                  // retryable
                else if (!(o instanceof CountedCompleter))
                    h = -1;                 // unmatchable
                else {
                    CountedCompleter<?> t = (CountedCompleter<?>)o;
                    for (CountedCompleter<?> r = t;;) {
                        if (r == task) {
                            if (base == b &&
                                U.compareAndSwapObject(a, j, t, null)) {
                                base = b + 1;
                                t.doExec();
                                h = 1;      // success
                            }
                            else
                                h = 2;      // lost CAS
                            break;
                        }
                        else if ((r = r.completer) == null) {
                            h = -1;         // unmatched
                            break;
                        }
                    }
                }
            }
            return h;
        }
        /**
         * Returns true if owned and not known to be blocked.
         */
        final boolean isApparentlyUnblocked() {
            Thread wt; Thread.State s;
            return (scanState >= 0 &&
                    (wt = owner) != null &&
                    (s = wt.getState()) != Thread.State.BLOCKED &&
                    s != Thread.State.WAITING &&
                    s != Thread.State.TIMED_WAITING);
        }
        // Unsafe mechanics. Note that some are (and must be) the same as in FJP
        private static final sun.misc.Unsafe U;
        private static final int  ABASE;
        private static final int  ASHIFT;
        private static final long QTOP;
        private static final long QLOCK;
        private static final long QCURRENTSTEAL;
        static {
            try {
                U = sun.misc.Unsafe.getUnsafe();
                Class<?> wk = WorkQueue.class;
                Class<?> ak = ForkJoinTask[].class;
                QTOP = U.objectFieldOffset
                    (wk.getDeclaredField("top"));
                QLOCK = U.objectFieldOffset
                    (wk.getDeclaredField("qlock"));
                QCURRENTSTEAL = U.objectFieldOffset
                    (wk.getDeclaredField("currentSteal"));
                ABASE = U.arrayBaseOffset(ak);
                int scale = U.arrayIndexScale(ak);
                if ((scale & (scale - 1)) != 0)
                    throw new Error("data type scale not a power of two");
                ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }
    // static fields (initialized in static initializer below)
    /**
     * Creates a new ForkJoinWorkerThread. This factory is used unless
     * overridden in ForkJoinPool constructors.
     */
    public static final ForkJoinWorkerThreadFactory
        defaultForkJoinWorkerThreadFactory;
    /**
     * Permission required for callers of methods that may start or
     * kill threads.
     */
    private static final RuntimePermission modifyThreadPermission;
    /**
     * Common (static) pool. Non-null for public use unless a static
     * construction exception, but internal usages null-check on use
     * to paranoically avoid potential initialization circularities
     * as well as to simplify generated code.
     */
    static final ForkJoinPool common;
    /**
     * Common pool parallelism. To allow simpler use and management
     * when common pool threads are disabled, we allow the underlying
     * common.parallelism field to be zero, but in that case still report
     * parallelism as 1 to reflect resulting caller-runs mechanics.
     */
    static final int commonParallelism;
    /**
     * Limit on spare thread construction in tryCompensate.
     */
    private static int commonMaxSpares;
    /**
     * Sequence number for creating workerNamePrefix.
     */
    private static int poolNumberSequence;
    /**
     * Returns the next sequence number. We don't expect this to
     * ever contend, so use simple builtin sync.
     */
    private static final synchronized int nextPoolId() {
        return ++poolNumberSequence;
    }
    // static configuration constants
    /**
     * Initial timeout value (in nanoseconds) for the thread
     * triggering quiescence to park waiting for new work. On timeout,
     * the thread will instead try to shrink the number of
     * workers. The value should be large enough to avoid overly
     * aggressive shrinkage during most transient stalls (long GCs
     * etc).
     */
    private static final long IDLE_TIMEOUT = 2000L * 1000L * 1000L; // 2sec
    /**
     * Tolerance for idle timeouts, to cope with timer undershoots
     */
    private static final long TIMEOUT_SLOP = 20L * 1000L * 1000L;  // 20ms
    /**
     * The initial value for commonMaxSpares during static
     * initialization. The value is far in excess of normal
     * requirements, but also far short of MAX_CAP and typical
     * OS thread limits, so allows JVMs to catch misuse/abuse
     * before running out of resources needed to do so.
     */
    private static final int DEFAULT_COMMON_MAX_SPARES = 256;
    /**
     * Number of times to spin-wait before blocking. The spins (in
     * awaitRunStateLock and awaitWork) currently use randomized
     * spins. Currently set to zero to reduce CPU usage.
     *
     * If greater than zero the value of SPINS must be a power
     * of two, at least 4.  A value of 2048 causes spinning for a
     * small fraction of typical context-switch times.
     *
     * If/when MWAIT-like intrinsics becomes available, they
     * may allow quieter spinning.
     */
    private static final int SPINS  = 0;
    /**
     * Increment for seed generators. See class ThreadLocal for
     * explanation.
     */
    private static final int SEED_INCREMENT = 0x9e3779b9;
    /*
     * Bits and masks for field ctl, packed with 4 16 bit subfields:
     * AC: Number of active running workers minus target parallelism
     * TC: Number of total workers minus target parallelism
     * SS: version count and status of top waiting thread
     * ID: poolIndex of top of Treiber stack of waiters
     *
     * When convenient, we can extract the lower 32 stack top bits
     * (including version bits) as sp=(int)ctl.  The offsets of counts
     * by the target parallelism and the positionings of fields makes
     * it possible to perform the most common checks via sign tests of
     * fields: When ac is negative, there are not enough active
     * workers, when tc is negative, there are not enough total
     * workers.  When sp is non-zero, there are waiting workers.  To
     * deal with possibly negative fields, we use casts in and out of
     * "short" and/or signed shifts to maintain signedness.
     *
     * Because it occupies uppermost bits, we can add one active count
     * using getAndAddLong of AC_UNIT, rather than CAS, when returning
     * from a blocked join.  Other updates entail multiple subfields
     * and masking, requiring CAS.
     */
    // Lower and upper word masks
    private static final long SP_MASK    = 0xffffffffL;
    private static final long UC_MASK    = ~SP_MASK;
    // Active counts
    private static final int  AC_SHIFT   = 48;
    private static final long AC_UNIT    = 0x0001L << AC_SHIFT;
    private static final long AC_MASK    = 0xffffL << AC_SHIFT;
    // Total counts
    private static final int  TC_SHIFT   = 32;
    private static final long TC_UNIT    = 0x0001L << TC_SHIFT;
    private static final long TC_MASK    = 0xffffL << TC_SHIFT;
    private static final long ADD_WORKER = 0x0001L << (TC_SHIFT + 15); // sign
    // runState bits: SHUTDOWN must be negative, others arbitrary powers of two
    private static final int  RSLOCK     = 1;
    private static final int  RSIGNAL    = 1 << 1;
    private static final int  STARTED    = 1 << 2;
    private static final int  STOP       = 1 << 29;
    private static final int  TERMINATED = 1 << 30;
    private static final int  SHUTDOWN   = 1 << 31;
    // Instance fields
    volatile long ctl;                   // main pool control
    volatile int runState;               // lockable status
    final int config;                    // parallelism, mode
    int indexSeed;                       // to generate worker index
    volatile WorkQueue[] workQueues;     // main registry
    final ForkJoinWorkerThreadFactory factory;
    final UncaughtExceptionHandler ueh // per-worker UEH
    final String workerNamePrefix;       // to create worker name string
    volatile AtomicLong stealCounter;    // also used as sync monitor
    /**
     * Acquires the runState lock; returns current (locked) runState.
     */
    private int lockRunState() {
        int rs;
        return ((((rs = runState) & RSLOCK) != 0 ||
                 !U.compareAndSwapInt(this, RUNSTATE, rs, rs |= RSLOCK)) ?
                awaitRunStateLock() : rs);
    }
    /**
     * Spins and/or blocks until runstate lock is available.  See
     * above for explanation.
     */
    private int awaitRunStateLock() {
        Object lock;
        boolean wasInterrupted = false;
        for (int spins = SPINS, r = 0, rs, ns;;) {
            if (((rs = runState) & RSLOCK) == 0) {
                if (U.compareAndSwapInt(this, RUNSTATE, rs, ns = rs | RSLOCK)) {
                    if (wasInterrupted) {
                        try {
                            Thread.currentThread().interrupt();
                        } catch (SecurityException ignore) {
                        }
                    }
                    return ns;
                }
            }
            else if (r == 0)
                r = ThreadLocalRandom.nextSecondarySeed();
            else if (spins > 0) {
                r ^= r << 6; r ^= r >>> 21; r ^= r << 7; // xorshift
                if (r >= 0)
                    --spins;
            }
            else if ((rs & STARTED) == 0 || (lock = stealCounter) == null)
                Thread.yield();   // initialization race
            else if (U.compareAndSwapInt(this, RUNSTATE, rs, rs | RSIGNAL)) {
                synchronized (lock) {
                    if ((runState & RSIGNAL) != 0) {
                        try {
                            lock.wait();
                        } catch (InterruptedException ie) {
                            if (!(Thread.currentThread() instanceof
                                  ForkJoinWorkerThread))
                                wasInterrupted = true;
                        }
                    }
                    else
                        lock.notifyAll();
                }
            }
        }
    }
    /**
     * Unlocks and sets runState to newRunState.
     *
     * @param oldRunState a value returned from lockRunState
     * @param newRunState the next value (must have lock bit clear).
     */
    private void unlockRunState(int oldRunState, int newRunState) {
        if (!U.compareAndSwapInt(this, RUNSTATE, oldRunState, newRunState)) {
            Object lock = stealCounter;
            runState = newRunState;              // clears RSIGNAL bit
            if (lock != null)
                synchronized (lock) { lock.notifyAll(); }
        }
    }
    // Creating, registering and deregistering workers
    /**
     * Tries to construct and start one worker. Assumes that total
     * count has already been incremented as a reservation.  Invokes
     * deregisterWorker on any failure.
     *
     * @return true if successful
     */
    private boolean createWorker() {
        ForkJoinWorkerThreadFactory fac = factory;
        Throwable ex = null;
        ForkJoinWorkerThread wt = null;
        try {
            if (fac != null && (wt = fac.newThread(this)) != null) {
                wt.start();
                return true;
            }
        } catch (Throwable rex) {
            ex = rex;
        }
        deregisterWorker(wt, ex);
        return false;
    }
    /**
     * Tries to add one worker, incrementing ctl counts before doing
     * so, relying on createWorker to back out on failure.
     *
     * @param c incoming ctl value, with total count negative and no
     * idle workers.  On CAS failure, c is refreshed and retried if
     * this holds (otherwise, a new worker is not needed).
     */
    private void tryAddWorker(long c) {
        boolean add = false;
        do {
            long nc = ((AC_MASK & (c + AC_UNIT)) |
                       (TC_MASK & (c + TC_UNIT)));
            if (ctl == c) {
                int rs, stop;                 // check if terminating
                if ((stop = (rs = lockRunState()) & STOP) == 0)
                    add = U.compareAndSwapLong(this, CTL, c, nc);
                unlockRunState(rs, rs & ~RSLOCK);
                if (stop != 0)
                    break;
                if (add) {
                    createWorker();
                    break;
                }
            }
        } while (((c = ctl) & ADD_WORKER) != 0L && (int)c == 0);
    }
    /**
     * Callback from ForkJoinWorkerThread constructor to establish and
     * record its WorkQueue.
     *
     * @param wt the worker thread
     * @return the worker's queue
     */
    final WorkQueue registerWorker(ForkJoinWorkerThread wt) {
        UncaughtExceptionHandler handler;
        wt.setDaemon(true);                           // configure thread
        if ((handler = ueh) != null)
            wt.setUncaughtExceptionHandler(handler);
        WorkQueue w = new WorkQueue(this, wt);
        int i = 0;                                    // assign a pool index
        int mode = config & MODE_MASK;
        int rs = lockRunState();
        try {
            WorkQueue[] ws; int n;                    // skip if no array
            if ((ws = workQueues) != null && (n = ws.length) > 0) {
                int s = indexSeed += SEED_INCREMENT // unlikely to collide
                int m = n - 1;
                i = ((s << 1) | 1) & m;               // odd-numbered indices
                if (ws[i] != null) {                  // collision
                    int probes = 0;                   // step by approx half n
                    int step = (n <= 4) ? 2 : ((n >>> 1) & EVENMASK) + 2;
                    while (ws[i = (i + step) & m] != null) {
                        if (++probes >= n) {
                            workQueues = ws = Arrays.copyOf(ws, n <<= 1);
                            m = n - 1;
                            probes = 0;
                        }
                    }
                }
                w.hint = s;                           // use as random seed
                w.config = i | mode;
                w.scanState = i;                      // publication fence
                ws[i] = w;
            }
        } finally {
            unlockRunState(rs, rs & ~RSLOCK);
        }
        wt.setName(workerNamePrefix.concat(Integer.toString(i >>> 1)));
        return w;
    }
    /**
     * Final callback from terminating worker, as well as upon failure
     * to construct or start a worker.  Removes record of worker from
     * array, and adjusts counts. If pool is shutting down, tries to
     * complete termination.
     *
     * @param wt the worker thread, or null if construction failed
     * @param ex the exception causing failure, or null if none
     */
    final void deregisterWorker(ForkJoinWorkerThread wt, Throwable ex) {
        WorkQueue w = null;
        if (wt != null && (w = wt.workQueue) != null) {
            WorkQueue[] ws;                           // remove index from array
            int idx = w.config & SMASK;
            int rs = lockRunState();
            if ((ws = workQueues) != null && ws.length > idx && ws[idx] == w)
                ws[idx] = null;
            unlockRunState(rs, rs & ~RSLOCK);
        }
        long c;                                       // decrement counts
        do {} while (!U.compareAndSwapLong
                     (this, CTL, c = ctl, ((AC_MASK & (c - AC_UNIT)) |
                                           (TC_MASK & (c - TC_UNIT)) |
                                           (SP_MASK & c))));
        if (w != null) {
            w.qlock = -1;                             // ensure set
            w.transferStealCount(this);
            w.cancelAll();                            // cancel remaining tasks
        }
        for (;;) {                                    // possibly replace
            WorkQueue[] ws; int m, sp;
            if (tryTerminate(false, false) || w == null || w.array == null ||
                (runState & STOP) != 0 || (ws = workQueues) == null ||
                (m = ws.length - 1) < 0)              // already terminating
                break;
            if ((sp = (int)(c = ctl)) != 0) {         // wake up replacement
                if (tryRelease(c, ws[sp & m], AC_UNIT))
                    break;
            }
            else if (ex != null && (c & ADD_WORKER) != 0L) {
                tryAddWorker(c);                      // create replacement
                break;
            }
            else                                      // don't need replacement
                break;
        }
        if (ex == null)                               // help clean on way out
            ForkJoinTask.helpExpungeStaleExceptions();
        else                                          // rethrow
            ForkJoinTask.rethrow(ex);
    }
    // Signalling
    /**
     * Tries to create or activate a worker if too few are active.
     *
     * @param ws the worker array to use to find signallees
     * @param q a WorkQueue --if non-null, don't retry if now empty
     */
    final void signalWork(WorkQueue[] ws, WorkQueue q) {
        long c; int sp, i; WorkQueue v; Thread p;
        while ((c = ctl) < 0L) {                       // too few active
            if ((sp = (int)c) == 0) {                  // no idle workers
                if ((c & ADD_WORKER) != 0L)            // too few workers
                    tryAddWorker(c);
                break;
            }
            if (ws == null)                            // unstarted/terminated
                break;
            if (ws.length <= (i = sp & SMASK))         // terminated
                break;
            if ((v = ws[i]) == null)                   // terminating
                break;
            int vs = (sp + SS_SEQ) & ~INACTIVE;        // next scanState
            int d = sp - v.scanState;                  // screen CAS
            long nc = (UC_MASK & (c + AC_UNIT)) | (SP_MASK & v.stackPred);
            if (d == 0 && U.compareAndSwapLong(this, CTL, c, nc)) {
                v.scanState = vs;                      // activate v
                if ((p = v.parker) != null)
                    U.unpark(p);
                break;
            }
            if (q != null && q.base == q.top)          // no more work
                break;
        }
    }
    /**
     * Signals and releases worker v if it is top of idle worker
     * stack.  This performs a one-shot version of signalWork only if
     * there is (apparently) at least one idle worker.
     *
     * @param c incoming ctl value
     * @param v if non-null, a worker
     * @param inc the increment to active count (zero when compensating)
     * @return true if successful
     */
    private boolean tryRelease(long c, WorkQueue v, long inc) {
        int sp = (int)c, vs = (sp + SS_SEQ) & ~INACTIVE; Thread p;
        if (v != null && v.scanState == sp) {          // v is at top of stack
            long nc = (UC_MASK & (c + inc)) | (SP_MASK & v.stackPred);
            if (U.compareAndSwapLong(this, CTL, c, nc)) {
                v.scanState = vs;
                if ((p = v.parker) != null)
                    U.unpark(p);
                return true;
            }
        }
        return false;
    }
    // Scanning for tasks
    /**
     * Top-level runloop for workers, called by ForkJoinWorkerThread.run.
     */
    final void runWorker(WorkQueue w) {
        w.growArray();                   // allocate queue
        int seed = w.hint;               // initially holds randomization hint
        int r = (seed == 0) ? 1 : seed // avoid 0 for xorShift
        for (ForkJoinTask<?> t;;) {
            if ((t = scan(w, r)) != null)
                w.runTask(t);
            else if (!awaitWork(w, r))
                break;
            r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift
        }
    }
    /**
     * Scans for and tries to steal a top-level task. Scans start at a
     * random location, randomly moving on apparent contention,
     * otherwise continuing linearly until reaching two consecutive
     * empty passes over all queues with the same checksum (summing
     * each base index of each queue, that moves on each steal), at
     * which point the worker tries to inactivate and then re-scans,
     * attempting to re-activate (itself or some other worker) if
     * finding a task; otherwise returning null to await work.  Scans
     * otherwise touch as little memory as possible, to reduce
     * disruption on other scanning threads.
     *
     * @param w the worker (via its WorkQueue)
     * @param r a random seed
     * @return a task, or null if none found
     */
    private ForkJoinTask<?> scan(WorkQueue w, int r) {
        WorkQueue[] ws; int m;
        if ((ws = workQueues) != null && (m = ws.length - 1) > 0 && w != null) {
            int ss = w.scanState;                     // initially non-negative
            for (int origin = r & m, k = origin, oldSum = 0, checkSum = 0;;) {
                WorkQueue q; ForkJoinTask<?>[] a; ForkJoinTask<?> t;
                int b, n; long c;
                if ((q = ws[k]) != null) {
                    if ((n = (b = q.base) - q.top) < 0 &&
                        (a = q.array) != null) {      // non-empty
                        long i = (((a.length - 1) & b) << ASHIFT) + ABASE;
                        if ((t = ((ForkJoinTask<?>)
                                  U.getObjectVolatile(a, i))) != null &&
                            q.base == b) {
                            if (ss >= 0) {
                                if (U.compareAndSwapObject(a, i, t, null)) {
                                    q.base = b + 1;
                                    if (n < -1)       // signal others
                                        signalWork(ws, q);
                                    return t;
                                }
                            }
                            else if (oldSum == 0 &&   // try to activate
                                     w.scanState < 0)
                                tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
                        }
                        if (ss < 0)                   // refresh
                            ss = w.scanState;
                        r ^= r << 1; r ^= r >>> 3; r ^= r << 10;
                        origin = k = r & m;           // move and rescan
                        oldSum = checkSum = 0;
                        continue;
                    }
                    checkSum += b;
                }
                if ((k = (k + 1) & m) == origin) {    // continue until stable
                    if ((ss >= 0 || (ss == (ss = w.scanState))) &&
                        oldSum == (oldSum = checkSum)) {
                        if (ss < 0 || w.qlock < 0)    // already inactive
                            break;
                        int ns = ss | INACTIVE;       // try to inactivate
                        long nc = ((SP_MASK & ns) |
                                   (UC_MASK & ((c = ctl) - AC_UNIT)));
                        w.stackPred = (int)c;         // hold prev stack top
                        U.putInt(w, QSCANSTATE, ns);
                        if (U.compareAndSwapLong(this, CTL, c, nc))
                            ss = ns;
                        else
                            w.scanState = ss;         // back out
                    }
                    checkSum = 0;
                }
            }
        }
        return null;
    }
    /**
     * Possibly blocks worker w waiting for a task to steal, or
     * returns false if the worker should terminate.  If inactivating
     * w has caused the pool to become quiescent, checks for pool
     * termination, and, so long as this is not the only worker, waits
     * for up to a given duration.  On timeout, if ctl has not
     * changed, terminates the worker, which will in turn wake up
     * another worker to possibly repeat this process.
     *
     * @param w the calling worker
     * @param r a random seed (for spins)
     * @return false if the worker should terminate
     */
    private boolean awaitWork(WorkQueue w, int r) {
        if (w == null || w.qlock < 0)                 // w is terminating
            return false;
        for (int pred = w.stackPred, spins = SPINS, ss;;) {
            if ((ss = w.scanState) >= 0)
                break;
            else if (spins > 0) {
                r ^= r << 6; r ^= r >>> 21; r ^= r << 7;
                if (r >= 0 && --spins == 0) {         // randomize spins
                    WorkQueue v; WorkQueue[] ws; int s, j; AtomicLong sc;
                    if (pred != 0 && (ws = workQueues) != null &&
                        (j = pred & SMASK) < ws.length &&
                        (v = ws[j]) != null &&        // see if pred parking
                        (v.parker == null || v.scanState >= 0))
                        spins = SPINS;                // continue spinning
                }
            }
            else if (w.qlock < 0)                     // recheck after spins
                return false;
            else if (!Thread.interrupted()) {
                long c, prevctl, parkTime, deadline;
                int ac = (int)((c = ctl) >> AC_SHIFT) + (config & SMASK);
                if ((ac <= 0 && tryTerminate(false, false)) ||
                    (runState & STOP) != 0)           // pool terminating
                    return false;
                if (ac <= 0 && ss == (int)c) {        // is last waiter
                    prevctl = (UC_MASK & (c + AC_UNIT)) | (SP_MASK & pred);
                    int t = (short)(c >>> TC_SHIFT);  // shrink excess spares
                    if (t > 2 && U.compareAndSwapLong(this, CTL, c, prevctl))
                        return false;                 // else use timed wait
                    parkTime = IDLE_TIMEOUT * ((t >= 0) ? 1 : 1 - t);
                    deadline = System.nanoTime() + parkTime - TIMEOUT_SLOP;
                }
                else
                    prevctl = parkTime = deadline = 0L;
                Thread wt = Thread.currentThread();
                U.putObject(wt, PARKBLOCKER, this);   // emulate LockSupport
                w.parker = wt;
                if (w.scanState < 0 && ctl == c)      // recheck before park
                    U.park(false, parkTime);
                U.putOrderedObject(w, QPARKER, null);
                U.putObject(wt, PARKBLOCKER, null);
                if (w.scanState >= 0)
                    break;
                if (parkTime != 0L && ctl == c &&
                    deadline - System.nanoTime() <= 0L &&
                    U.compareAndSwapLong(this, CTL, c, prevctl))
                    return false;                     // shrink pool
            }
        }
        return true;
    }
    // Joining tasks
    /**
     * Tries to steal and run tasks within the target's computation.
     * Uses a variant of the top-level algorithm, restricted to tasks
     * with the given task as ancestor: It prefers taking and running
     * eligible tasks popped from the worker's own queue (via
     * popCC). Otherwise it scans others, randomly moving on
     * contention or execution, deciding to give up based on a
     * checksum (via return codes frob pollAndExecCC). The maxTasks
     * argument supports external usages; internal calls use zero,
     * allowing unbounded steps (external calls trap non-positive
     * values).
     *
     * @param w caller
     * @param maxTasks if non-zero, the maximum number of other tasks to run
     * @return task status on exit
     */
    final int helpComplete(WorkQueue w, CountedCompleter<?> task,
                           int maxTasks) {
        WorkQueue[] ws; int s = 0, m;
        if ((ws = workQueues) != null && (m = ws.length - 1) >= 0 &&
            task != null && w != null) {
            int mode = w.config;                 // for popCC
            int r = w.hint ^ w.top;              // arbitrary seed for origin
            int origin = r & m;                  // first queue to scan
            int h = 1;                           // 1:ran, >1:contended, <0:hash
            for (int k = origin, oldSum = 0, checkSum = 0;;) {
                CountedCompleter<?> p; WorkQueue q;
                if ((s = task.status) < 0)
                    break;
                if (h == 1 && (p = w.popCC(task, mode)) != null) {
                    p.doExec();                  // run local task
                    if (maxTasks != 0 && --maxTasks == 0)
                        break;
                    origin = k;                  // reset
                    oldSum = checkSum = 0;
                }
                else {                           // poll other queues
                    if ((q = ws[k]) == null)
                        h = 0;
                    else if ((h = q.pollAndExecCC(task)) < 0)
                        checkSum += h;
                    if (h > 0) {
                        if (h == 1 && maxTasks != 0 && --maxTasks == 0)
                            break;
                        r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift
                        origin = k = r & m;      // move and restart
                        oldSum = checkSum = 0;
                    }
                    else if ((k = (k + 1) & m) == origin) {
                        if (oldSum == (oldSum = checkSum))
                            break;
                        checkSum = 0;
                    }
                }
            }
        }
        return s;
    }
    /**
     * Tries to locate and execute tasks for a stealer of the given
     * task, or in turn one of its stealers, Traces currentSteal ->
     * currentJoin links looking for a thread working on a descendant
     * of the given task and with a non-empty queue to steal back and
     * execute tasks from. The first call to this method upon a
     * waiting join will often entail scanning/search, (which is OK
     * because the joiner has nothing better to do), but this method
     * leaves hints in workers to speed up subsequent calls.
     *
     * @param w caller
     * @param task the task to join
     */
    private void helpStealer(WorkQueue w, ForkJoinTask<?> task) {
        WorkQueue[] ws = workQueues;
        int oldSum = 0, checkSum, m;
        if (ws != null && (m = ws.length - 1) >= 0 && w != null &&
            task != null) {
            do {                                       // restart point
                checkSum = 0;                          // for stability check
                ForkJoinTask<?> subtask;
                WorkQueue j = w, v;                    // v is subtask stealer
                descent: for (subtask = task; subtask.status >= 0; ) {
                    for (int h = j.hint | 1, k = 0, i; ; k += 2) {
                        if (k > m)                     // can't find stealer
                            break descent;
                        if ((v = ws[i = (h + k) & m]) != null) {
                            if (v.currentSteal == subtask) {
                                j.hint = i;
                                break;
                            }
                            checkSum += v.base;
                        }
                    }
                    for (;;) {                         // help v or descend
                        ForkJoinTask<?>[] a; int b;
                        checkSum += (b = v.base);
                        ForkJoinTask<?> next = v.currentJoin;
                        if (subtask.status < 0 || j.currentJoin != subtask ||
                            v.currentSteal != subtask) // stale
                            break descent;
                        if (b - v.top >= 0 || (a = v.array) == null) {
                            if ((subtask = next) == null)
                                break descent;
                            j = v;
                            break;
                        }
                        int i = (((a.length - 1) & b) << ASHIFT) + ABASE;
                        ForkJoinTask<?> t = ((ForkJoinTask<?>)
                                             U.getObjectVolatile(a, i));
                        if (v.base == b) {
                            if (t == null)             // stale
                                break descent;
                            if (U.compareAndSwapObject(a, i, t, null)) {
                                v.base = b + 1;
                                ForkJoinTask<?> ps = w.currentSteal;
                                int top = w.top;
                                do {
                                    U.putOrderedObject(w, QCURRENTSTEAL, t);
                                    t.doExec();        // clear local tasks too
                                } while (task.status >= 0 &&
                                         w.top != top &&
                                         (t = w.pop()) != null);
                                U.putOrderedObject(w, QCURRENTSTEAL, ps);
                                if (w.base != w.top)
                                    return;            // can't further help
                            }
                        }
                    }
                }
            } while (task.status >= 0 && oldSum != (oldSum = checkSum));
        }
    }
    /**
     * Tries to decrement active count (sometimes implicitly) and
     * possibly release or create a compensating worker in preparation
     * for blocking. Returns false (retryable by caller), on
     * contention, detected staleness, instability, or termination.
     *
     * @param w caller
     */
    private boolean tryCompensate(WorkQueue w) {
        boolean canBlock;
        WorkQueue[] ws; long c; int m, pc, sp;
        if (w == null || w.qlock < 0 ||           // caller terminating
            (ws = workQueues) == null || (m = ws.length - 1) <= 0 ||
            (pc = config & SMASK) == 0)           // parallelism disabled
            canBlock = false;
        else if ((sp = (int)(c = ctl)) != 0)      // release idle worker
            canBlock = tryRelease(c, ws[sp & m], 0L);
        else {
            int ac = (int)(c >> AC_SHIFT) + pc;
            int tc = (short)(c >> TC_SHIFT) + pc;
            int nbusy = 0;                        // validate saturation
            for (int i = 0; i <= m; ++i) {        // two passes of odd indices
                WorkQueue v;
                if ((v = ws[((i << 1) | 1) & m]) != null) {
                    if ((v.scanState & SCANNING) != 0)
                        break;
                    ++nbusy;
                }
            }
            if (nbusy != (tc << 1) || ctl != c)
                canBlock = false;                 // unstable or stale
            else if (tc >= pc && ac > 1 && w.isEmpty()) {
                long nc = ((AC_MASK & (c - AC_UNIT)) |
                           (~AC_MASK & c));       // uncompensated
                canBlock = U.compareAndSwapLong(this, CTL, c, nc);
            }
            else if (tc >= MAX_CAP ||
                     (this == common && tc >= pc + commonMaxSpares))
                throw new RejectedExecutionException(
                    "Thread limit exceeded replacing blocked worker");
            else {                                // similar to tryAddWorker
                boolean add = false; int rs;      // CAS within lock
                long nc = ((AC_MASK & c) |
                           (TC_MASK & (c + TC_UNIT)));
                if (((rs = lockRunState()) & STOP) == 0)
                    add = U.compareAndSwapLong(this, CTL, c, nc);
                unlockRunState(rs, rs & ~RSLOCK);
                canBlock = add && createWorker(); // throws on exception
            }
        }
        return canBlock;
    }
    /**
     * Helps and/or blocks until the given task is done or timeout.
     *
     * @param w caller
     * @param task the task
     * @param deadline for timed waits, if nonzero
     * @return task status on exit
     */
    final int awaitJoin(WorkQueue w, ForkJoinTask<?> task, long deadline) {
        int s = 0;
        if (task != null && w != null) {
            ForkJoinTask<?> prevJoin = w.currentJoin;
            U.putOrderedObject(w, QCURRENTJOIN, task);
            CountedCompleter<?> cc = (task instanceof CountedCompleter) ?
                (CountedCompleter<?>)task : null;
            for (;;) {
                if ((s = task.status) < 0)
                    break;
                if (cc != null)
                    helpComplete(w, cc, 0);
                else if (w.base == w.top || w.tryRemoveAndExec(task))
                    helpStealer(w, task);
                if ((s = task.status) < 0)
                    break;
                long ms, ns;
                if (deadline == 0L)
                    ms = 0L;
                else if ((ns = deadline - System.nanoTime()) <= 0L)
                    break;
                else if ((ms = TimeUnit.NANOSECONDS.toMillis(ns)) <= 0L)
                    ms = 1L;
                if (tryCompensate(w)) {
                    task.internalWait(ms);
                    U.getAndAddLong(this, CTL, AC_UNIT);
                }
            }
            U.putOrderedObject(w, QCURRENTJOIN, prevJoin);
        }
        return s;
    }
    // Specialized scanning
    /**
     * Returns a (probably) non-empty steal queue, if one is found
     * during a scan, else null.  This method must be retried by
     * caller if, by the time it tries to use the queue, it is empty.
     */
    private WorkQueue findNonEmptyStealQueue() {
        WorkQueue[] ws; int m // one-shot version of scan loop
        int r = ThreadLocalRandom.nextSecondarySeed();
        if ((ws = workQueues) != null && (m = ws.length - 1) >= 0) {
            for (int origin = r & m, k = origin, oldSum = 0, checkSum = 0;;) {
                WorkQueue q; int b;
                if ((q = ws[k]) != null) {
                    if ((b = q.base) - q.top < 0)
                        return q;
                    checkSum += b;
                }
                if ((k = (k + 1) & m) == origin) {
                    if (oldSum == (oldSum = checkSum))
                        break;
                    checkSum = 0;
                }
            }
        }
        return null;
    }
    /**
     * Runs tasks until {@code isQuiescent()}. We piggyback on
     * active count ctl maintenance, but rather than blocking
     * when tasks cannot be found, we rescan until all others cannot
     * find tasks either.
     */
    final void helpQuiescePool(WorkQueue w) {
        ForkJoinTask<?> ps = w.currentSteal; // save context
        for (boolean active = true;;) {
            long c; WorkQueue q; ForkJoinTask<?> t; int b;
            w.execLocalTasks();     // run locals before each scan
            if ((q = findNonEmptyStealQueue()) != null) {
                if (!active) {      // re-establish active count
                    active = true;
                    U.getAndAddLong(this, CTL, AC_UNIT);
                }
                if ((b = q.base) - q.top < 0 && (t = q.pollAt(b)) != null) {
                    U.putOrderedObject(w, QCURRENTSTEAL, t);
                    t.doExec();
                    if (++w.nsteals < 0)
                        w.transferStealCount(this);
                }
            }
            else if (active) {      // decrement active count without queuing
                long nc = (AC_MASK & ((c = ctl) - AC_UNIT)) | (~AC_MASK & c);
                if ((int)(nc >> AC_SHIFT) + (config & SMASK) <= 0)
                    break;          // bypass decrement-then-increment
                if (U.compareAndSwapLong(this, CTL, c, nc))
                    active = false;
            }
            else if ((int)((c = ctl) >> AC_SHIFT) + (config & SMASK) <= 0 &&
                     U.compareAndSwapLong(this, CTL, c, c + AC_UNIT))
                break;
        }
        U.putOrderedObject(w, QCURRENTSTEAL, ps);
    }
    /**
     * Gets and removes a local or stolen task for the given worker.
     *
     * @return a task, if available
     */
    final ForkJoinTask<?> nextTaskFor(WorkQueue w) {
        for (ForkJoinTask<?> t;;) {
            WorkQueue q; int b;
            if ((t = w.nextLocalTask()) != null)
                return t;
            if ((q = findNonEmptyStealQueue()) == null)
                return null;
            if ((b = q.base) - q.top < 0 && (t = q.pollAt(b)) != null)
                return t;
        }
    }
    /**
     * Returns a cheap heuristic guide for task partitioning when
     * programmers, frameworks, tools, or languages have little or no
     * idea about task granularity.  In essence, by offering this
     * method, we ask users only about tradeoffs in overhead vs
     * expected throughput and its variance, rather than how finely to
     * partition tasks.
     *
     * In a steady state strict (tree-structured) computation, each
     * thread makes available for stealing enough tasks for other
     * threads to remain active. Inductively, if all threads play by
     * the same rules, each thread should make available only a
     * constant number of tasks.
     *
     * The minimum useful constant is just 1. But using a value of 1
     * would require immediate replenishment upon each steal to
     * maintain enough tasks, which is infeasible.  Further,
     * partitionings/granularities of offered tasks should minimize
     * steal rates, which in general means that threads nearer the top
     * of computation tree should generate more than those nearer the
     * bottom. In perfect steady state, each thread is at
     * approximately the same level of computation tree. However,
     * producing extra tasks amortizes the uncertainty of progress and
     * diffusion assumptions.
     *
     * So, users will want to use values larger (but not much larger)
     * than 1 to both smooth over transient shortages and hedge
     * against uneven progress; as traded off against the cost of
     * extra task overhead. We leave the user to pick a threshold
     * value to compare with the results of this call to guide
     * decisions, but recommend values such as 3.
     *
     * When all threads are active, it is on average OK to estimate
     * surplus strictly locally. In steady-state, if one thread is
     * maintaining say 2 surplus tasks, then so are others. So we can
     * just use estimated queue length.  However, this strategy alone
     * leads to serious mis-estimates in some non-steady-state
     * conditions (ramp-up, ramp-down, other stalls). We can detect
     * many of these by further considering the number of "idle"
     * threads, that are known to have zero queued tasks, so
     * compensate by a factor of (#idle/#active) threads.
     */
    static int getSurplusQueuedTaskCount() {
        Thread t; ForkJoinWorkerThread wt; ForkJoinPool pool; WorkQueue q;
        if (((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)) {
            int p = (pool = (wt = (ForkJoinWorkerThread)t).pool).
                config & SMASK;
            int n = (q = wt.workQueue).top - q.base;
            int a = (int)(pool.ctl >> AC_SHIFT) + p;
            return n - (a > (p >>>= 1) ? 0 :
                        a > (p >>>= 1) ? 1 :
                        a > (p >>>= 1) ? 2 :
                        a > (p >>>= 1) ? 4 :
                        8);
        }
        return 0;
    }
    //  Termination
    /**
     * Possibly initiates and/or completes termination.
     *
     * @param now if true, unconditionally terminate, else only
     * if no work and no active workers
     * @param enable if true, enable shutdown when next possible
     * @return true if now terminating or terminated
     */
    private boolean tryTerminate(boolean now, boolean enable) {
        int rs;
        if (this == common)                       // cannot shut down
            return false;
        if ((rs = runState) >= 0) {
            if (!enable)
                return false;
            rs = lockRunState();                  // enter SHUTDOWN phase
            unlockRunState(rs, (rs & ~RSLOCK) | SHUTDOWN);
        }
        if ((rs & STOP) == 0) {
            if (!now) {                           // check quiescence
                for (long oldSum = 0L;;) {        // repeat until stable
                    WorkQueue[] ws; WorkQueue w; int m, b; long c;
                    long checkSum = ctl;
                    if ((int)(checkSum >> AC_SHIFT) + (config & SMASK) > 0)
                        return false;             // still active workers
                    if ((ws = workQueues) == null || (m = ws.length - 1) <= 0)
                        break;                    // check queues
                    for (int i = 0; i <= m; ++i) {
                        if ((w = ws[i]) != null) {
                            if ((b = w.base) != w.top || w.scanState >= 0 ||
                                w.currentSteal != null) {
                                tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
                                return false;     // arrange for recheck
                            }
                            checkSum += b;
                            if ((i & 1) == 0)
                                w.qlock = -1;     // try to disable external
                        }
                    }
                    if (oldSum == (oldSum = checkSum))
                        break;
                }
            }
            if ((runState & STOP) == 0) {
                rs = lockRunState();              // enter STOP phase
                unlockRunState(rs, (rs & ~RSLOCK) | STOP);
            }
        }
        int pass = 0;                             // 3 passes to help terminate
        for (long oldSum = 0L;;) {                // or until done or stable
            WorkQueue[] ws; WorkQueue w; ForkJoinWorkerThread wt; int m;
            long checkSum = ctl;
            if ((short)(checkSum >>> TC_SHIFT) + (config & SMASK) <= 0 ||
                (ws = workQueues) == null || (m = ws.length - 1) <= 0) {
                if ((runState & TERMINATED) == 0) {
                    rs = lockRunState();          // done
                    unlockRunState(rs, (rs & ~RSLOCK) | TERMINATED);
                    synchronized (this) { notifyAll(); } // for awaitTermination
                }
                break;
            }
            for (int i = 0; i <= m; ++i) {
                if ((w = ws[i]) != null) {
                    checkSum += w.base;
                    w.qlock = -1;                 // try to disable
                    if (pass > 0) {
                        w.cancelAll();            // clear queue
                        if (pass > 1 && (wt = w.owner) != null) {
                            if (!wt.isInterrupted()) {
                                try {             // unblock join
                                    wt.interrupt();
                                } catch (Throwable ignore) {
                                }
                            }
                            if (w.scanState < 0)
                                U.unpark(wt);     // wake up
                        }
                    }
                }
            }
            if (checkSum != oldSum) {             // unstable
                oldSum = checkSum;
                pass = 0;
            }
            else if (pass > 3 && pass > m)        // can't further help
                break;
            else if (++pass > 1) {                // try to dequeue
                long c; int j = 0, sp;            // bound attempts
                while (j++ <= m && (sp = (int)(c = ctl)) != 0)
                    tryRelease(c, ws[sp & m], AC_UNIT);
            }
        }
        return true;
    }
    // External operations
    /**
     * Full version of externalPush, handling uncommon cases, as well
     * as performing secondary initialization upon the first
     * submission of the first task to the pool.  It also detects
     * first submission by an external thread and creates a new shared
     * queue if the one at index if empty or contended.
     *
     * @param task the task. Caller must ensure non-null.
     */
    private void externalSubmit(ForkJoinTask<?> task) {
        int r;                                    // initialize caller's probe
        if ((r = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();
            r = ThreadLocalRandom.getProbe();
        }
        for (;;) {
            WorkQueue[] ws; WorkQueue q; int rs, m, k;
            boolean move = false;
            if ((rs = runState) < 0) {
                tryTerminate(false, false);     // help terminate
                throw new RejectedExecutionException();
            }
            else if ((rs & STARTED) == 0 ||     // initialize
                     ((ws = workQueues) == null || (m = ws.length - 1) < 0)) {
                int ns = 0;
                rs = lockRunState();
                try {
                    if ((rs & STARTED) == 0) {
                        U.compareAndSwapObject(this, STEALCOUNTER, null,
                                               new AtomicLong());
                        // create workQueues array with size a power of two
                        int p = config & SMASK; // ensure at least 2 slots
                        int n = (p > 1) ? p - 1 : 1;
                        n |= n >>> 1; n |= n >>> 2;  n |= n >>> 4;
                        n |= n >>> 8; n |= n >>> 16; n = (n + 1) << 1;
                        workQueues = new WorkQueue[n];
                        ns = STARTED;
                    }
                } finally {
                    unlockRunState(rs, (rs & ~RSLOCK) | ns);
                }
            }
            else if ((q = ws[k = r & m & SQMASK]) != null) {
                if (q.qlock == 0 && U.compareAndSwapInt(q, QLOCK, 0, 1)) {
                    ForkJoinTask<?>[] a = q.array;
                    int s = q.top;
                    boolean submitted = false; // initial submission or resizing
                    try {                      // locked version of push
                        if ((a != null && a.length > s + 1 - q.base) ||
                            (a = q.growArray()) != null) {
                            int j = (((a.length - 1) & s) << ASHIFT) + ABASE;
                            U.putOrderedObject(a, j, task);
                            U.putOrderedInt(q, QTOP, s + 1);
                            submitted = true;
                        }
                    } finally {
                        U.compareAndSwapInt(q, QLOCK, 1, 0);
                    }
                    if (submitted) {
                        signalWork(ws, q);
                        return;
                    }
                }
                move = true;                   // move on failure
            }
            else if (((rs = runState) & RSLOCK) == 0) { // create new queue
                q = new WorkQueue(this, null);
                q.hint = r;
                q.config = k | SHARED_QUEUE;
                q.scanState = INACTIVE;
                rs = lockRunState();           // publish index
                if (rs > 0 &&  (ws = workQueues) != null &&
                    k < ws.length && ws[k] == null)
                    ws[k] = q;                 // else terminated
                unlockRunState(rs, rs & ~RSLOCK);
            }
            else
                move = true;                   // move if busy
            if (move)
                r = ThreadLocalRandom.advanceProbe(r);
        }
    }
    /**
     * Tries to add the given task to a submission queue at
     * submitter's current queue. Only the (vastly) most common path
     * is directly handled in this method, while screening for need
     * for externalSubmit.
     *
     * @param task the task. Caller must ensure non-null.
     */
    final void externalPush(ForkJoinTask<?> task) {
        WorkQueue[] ws; WorkQueue q; int m;
        int r = ThreadLocalRandom.getProbe();
        int rs = runState;
        if ((ws = workQueues) != null && (m = (ws.length - 1)) >= 0 &&
            (q = ws[m & r & SQMASK]) != null && r != 0 && rs > 0 &&
            U.compareAndSwapInt(q, QLOCK, 0, 1)) {
            ForkJoinTask<?>[] a; int am, n, s;
            if ((a = q.array) != null &&
                (am = a.length - 1) > (n = (s = q.top) - q.base)) {
                int j = ((am & s) << ASHIFT) + ABASE;
                U.putOrderedObject(a, j, task);
                U.putOrderedInt(q, QTOP, s + 1);
                U.putIntVolatile(q, QLOCK, 0);
                if (n <= 1)
                    signalWork(ws, q);
                return;
            }
            U.compareAndSwapInt(q, QLOCK, 1, 0);
        }
        externalSubmit(task);
    }
    /**
     * Returns common pool queue for an external thread.
     */
    static WorkQueue commonSubmitterQueue() {
        ForkJoinPool p = common;
        int r = ThreadLocalRandom.getProbe();
        WorkQueue[] ws; int m;
        return (p != null && (ws = p.workQueues) != null &&
                (m = ws.length - 1) >= 0) ?
            ws[m & r & SQMASK] : null;
    }
    /**
     * Performs tryUnpush for an external submitter: Finds queue,
     * locks if apparently non-empty, validates upon locking, and
     * adjusts top. Each check can fail but rarely does.
     */
    final boolean tryExternalUnpush(ForkJoinTask<?> task) {
        WorkQueue[] ws; WorkQueue w; ForkJoinTask<?>[] a; int m, s;
        int r = ThreadLocalRandom.getProbe();
        if ((ws = workQueues) != null && (m = ws.length - 1) >= 0 &&
            (w = ws[m & r & SQMASK]) != null &&
            (a = w.array) != null && (s = w.top) != w.base) {
            long j = (((a.length - 1) & (s - 1)) << ASHIFT) + ABASE;
            if (U.compareAndSwapInt(w, QLOCK, 0, 1)) {
                if (w.top == s && w.array == a &&
                    U.getObject(a, j) == task &&
                    U.compareAndSwapObject(a, j, task, null)) {
                    U.putOrderedInt(w, QTOP, s - 1);
                    U.putOrderedInt(w, QLOCK, 0);
                    return true;
                }
                U.compareAndSwapInt(w, QLOCK, 1, 0);
            }
        }
        return false;
    }
    /**
     * Performs helpComplete for an external submitter.
     */
    final int externalHelpComplete(CountedCompleter<?> task, int maxTasks) {
        WorkQueue[] ws; int n;
        int r = ThreadLocalRandom.getProbe();
        return ((ws = workQueues) == null || (n = ws.length) == 0) ? 0 :
            helpComplete(ws[(n - 1) & r & SQMASK], task, maxTasks);
    }
    // Exported methods
    // Constructors
    /**
     * Creates a {@code ForkJoinPool} with parallelism equal to {@link
     * java.lang.Runtime#availableProcessors}, using the {@linkplain
     * #defaultForkJoinWorkerThreadFactory default thread factory},
     * no UncaughtExceptionHandler, and non-async LIFO processing mode.
     *
     * @throws SecurityException if a security manager exists and
     *         the caller is not permitted to modify threads
     *         because it does not hold {@link
     *         java.lang.RuntimePermission}{@code ("modifyThread")}
     */
    public ForkJoinPool() {
        this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
             defaultForkJoinWorkerThreadFactory, null, false);
    }
    /**
     * Creates a {@code ForkJoinPool} with the indicated parallelism
     * level, the {@linkplain
     * #defaultForkJoinWorkerThreadFactory default thread factory},
     * no UncaughtExceptionHandler, and non-async LIFO processing mode.
     *
     * @param parallelism the parallelism level
     * @throws IllegalArgumentException if parallelism less than or
     *         equal to zero, or greater than implementation limit
     * @throws SecurityException if a security manager exists and
     *         the caller is not permitted to modify threads
     *         because it does not hold {@link
     *         java.lang.RuntimePermission}{@code ("modifyThread")}
     */
    public ForkJoinPool(int parallelism) {
        this(parallelism, defaultForkJoinWorkerThreadFactory, null, false);
    }
    /**
     * Creates a {@code ForkJoinPool} with the given parameters.
     *
     * @param parallelism the parallelism level. For default value,
     * use {@link java.lang.Runtime#availableProcessors}.
     * @param factory the factory for creating new threads. For default value,
     * use {@link #defaultForkJoinWorkerThreadFactory}.
     * @param handler the handler for internal worker threads that
     * terminate due to unrecoverable errors encountered while executing
     * tasks. For default value, use {@code null}.
     * @param asyncMode if true,
     * establishes local first-in-first-out scheduling mode for forked
     * tasks that are never joined. This mode may be more appropriate
     * than default locally stack-based mode in applications in which
     * worker threads only process event-style asynchronous tasks.
     * For default value, use {@code false}.
     * @throws IllegalArgumentException if parallelism less than or
     *         equal to zero, or greater than implementation limit
     * @throws NullPointerException if the factory is null
     * @throws SecurityException if a security manager exists and
     *         the caller is not permitted to modify threads
     *         because it does not hold {@link
     *         java.lang.RuntimePermission}{@code ("modifyThread")}
     */
    public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }
    private static int checkParallelism(int parallelism) {
        if (parallelism <= 0 || parallelism > MAX_CAP)
            throw new IllegalArgumentException();
        return parallelism;
    }
    private static ForkJoinWorkerThreadFactory checkFactory
        (ForkJoinWorkerThreadFactory factory) {
        if (factory == null)
            throw new NullPointerException();
        return factory;
    }
    /**
     * Creates a {@code ForkJoinPool} with the given parameters, without
     * any security checks or parameter validation.  Invoked directly by
     * makeCommonPool.
     */
    private ForkJoinPool(int parallelism,
                         ForkJoinWorkerThreadFactory factory,
                         UncaughtExceptionHandler handler,
                         int mode,
                         String workerNamePrefix) {
        this.workerNamePrefix = workerNamePrefix;
        this.factory = factory;
        this.ueh = handler;
        this.config = (parallelism & SMASK) | mode;
        long np = (long)(-parallelism); // offset ctl counts
        this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
    }
    /**
     * Returns the common pool instance. This pool is statically
     * constructed; its run state is unaffected by attempts to {@link
     * #shutdown} or {@link #shutdownNow}. However this pool and any
     * ongoing processing are automatically terminated upon program
     * {@link System#exit}.  Any program that relies on asynchronous
     * task processing to complete before program termination should
     * invoke {@code commonPool().}{@link #awaitQuiescence awaitQuiescence},
     * before exit.
     *
     * @return the common pool instance
     * @since 1.8
     */
    public static ForkJoinPool commonPool() {
        // assert common != null : "static init error";
        return common;
    }
    // Execution methods
    /**
     * Performs the given task, returning its result upon completion.
     * If the computation encounters an unchecked Exception or Error,
     * it is rethrown as the outcome of this invocation.  Rethrown
     * exceptions behave in the same way as regular exceptions, but,
     * when possible, contain stack traces (as displayed for example
     * using {@code ex.printStackTrace()}) of both the current thread
     * as well as the thread actually encountering the exception;
     * minimally only the latter.
     *
     * @param task the task
     * @param <T> the type of the task's result
     * @return the task's result
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public <T> T invoke(ForkJoinTask<T> task) {
        if (task == null)
            throw new NullPointerException();
        externalPush(task);
        return task.join();
    }
    /**
     * Arranges for (asynchronous) execution of the given task.
     *
     * @param task the task
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public void execute(ForkJoinTask<?> task) {
        if (task == null)
            throw new NullPointerException();
        externalPush(task);
    }
    // AbstractExecutorService methods
    /**
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public void execute(Runnable task) {
        if (task == null)
            throw new NullPointerException();
        ForkJoinTask<?> job;
        if (task instanceof ForkJoinTask<?>) // avoid re-wrap
            job = (ForkJoinTask<?>) task;
        else
            job = new ForkJoinTask.RunnableExecuteAction(task);
        externalPush(job);
    }
    /**
     * Submits a ForkJoinTask for execution.
     *
     * @param task the task to submit
     * @param <T> the type of the task's result
     * @return the task
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
        if (task == null)
            throw new NullPointerException();
        externalPush(task);
        return task;
    }
    /**
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public <T> ForkJoinTask<T> submit(Callable<T> task) {
        ForkJoinTask<T> job = new ForkJoinTask.AdaptedCallable<T>(task);
        externalPush(job);
        return job;
    }
    /**
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public <T> ForkJoinTask<T> submit(Runnable task, T result) {
        ForkJoinTask<T> job = new ForkJoinTask.AdaptedRunnable<T>(task, result);
        externalPush(job);
        return job;
    }
    /**
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public ForkJoinTask<?> submit(Runnable task) {
        if (task == null)
            throw new NullPointerException();
        ForkJoinTask<?> job;
        if (task instanceof ForkJoinTask<?>) // avoid re-wrap
            job = (ForkJoinTask<?>) task;
        else
            job = new ForkJoinTask.AdaptedRunnableAction(task);
        externalPush(job);
        return job;
    }
    /**
     * @throws NullPointerException       {@inheritDoc}
     * @throws RejectedExecutionException {@inheritDoc}
     */
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) {
        // In previous versions of this class, this method constructed
        // a task to run ForkJoinTask.invokeAll, but now external
        // invocation of multiple tasks is at least as efficient.
        ArrayList<Future<T>> futures = new ArrayList<>(tasks.size());
        boolean done = false;
        try {
            for (Callable<T> t : tasks) {
                ForkJoinTask<T> f = new ForkJoinTask.AdaptedCallable<T>(t);
                futures.add(f);
                externalPush(f);
            }
            for (int i = 0, size = futures.size(); i < size; i++)
                ((ForkJoinTask<?>)futures.get(i)).quietlyJoin();
            done = true;
            return futures;
        } finally {
            if (!done)
                for (int i = 0, size = futures.size(); i < size; i++)
                    futures.get(i).cancel(false);
        }
    }
    /**
     * Returns the factory used for constructing new workers.
     *
     * @return the factory used for constructing new workers
     */
    public ForkJoinWorkerThreadFactory getFactory() {
        return factory;
    }
    /**
     * Returns the handler for internal worker threads that terminate
     * due to unrecoverable errors encountered while executing tasks.
     *
     * @return the handler, or {@code null} if none
     */
    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        return ueh;
    }
    /**
     * Returns the targeted parallelism level of this pool.
     *
     * @return the targeted parallelism level of this pool
     */
    public int getParallelism() {
        int par;
        return ((par = config & SMASK) > 0) ? par : 1;
    }
    /**
     * Returns the targeted parallelism level of the common pool.
     *
     * @return the targeted parallelism level of the common pool
     * @since 1.8
     */
    public static int getCommonPoolParallelism() {
        return commonParallelism;
    }
    /**
     * Returns the number of worker threads that have started but not
     * yet terminated.  The result returned by this method may differ
     * from {@link #getParallelism} when threads are created to
     * maintain parallelism when others are cooperatively blocked.
     *
     * @return the number of worker threads
     */
    public int getPoolSize() {
        return (config & SMASK) + (short)(ctl >>> TC_SHIFT);
    }
    /**
     * Returns {@code true} if this pool uses local first-in-first-out
     * scheduling mode for forked tasks that are never joined.
     *
     * @return {@code true} if this pool uses async mode
     */
    public boolean getAsyncMode() {
        return (config & FIFO_QUEUE) != 0;
    }
    /**
     * Returns an estimate of the number of worker threads that are
     * not blocked waiting to join tasks or for other managed
     * synchronization. This method may overestimate the
     * number of running threads.
     *
     * @return the number of worker threads
     */
    public int getRunningThreadCount() {
        int rc = 0;
        WorkQueue[] ws; WorkQueue w;
        if ((ws = workQueues) != null) {
            for (int i = 1; i < ws.length; i += 2) {
                if ((w = ws[i]) != null && w.isApparentlyUnblocked())
                    ++rc;
            }
        }
        return rc;
    }
    /**
     * Returns an estimate of the number of threads that are currently
     * stealing or executing tasks. This method may overestimate the
     * number of active threads.
     *
     * @return the number of active threads
     */
    public int getActiveThreadCount() {
        int r = (config & SMASK) + (int)(ctl >> AC_SHIFT);
        return (r <= 0) ? 0 : r; // suppress momentarily negative values
    }
    /**
     * Returns {@code true} if all worker threads are currently idle.
     * An idle worker is one that cannot obtain a task to execute
     * because none are available to steal from other threads, and
     * there are no pending submissions to the pool. This method is
     * conservative; it might not return {@code true} immediately upon
     * idleness of all threads, but will eventually become true if
     * threads remain inactive.
     *
     * @return {@code true} if all threads are currently idle
     */
    public boolean isQuiescent() {
        return (config & SMASK) + (int)(ctl >> AC_SHIFT) <= 0;
    }
    /**
     * Returns an estimate of the total number of tasks stolen from
     * one thread's work queue by another. The reported value
     * underestimates the actual total number of steals when the pool
     * is not quiescent. This value may be useful for monitoring and
     * tuning fork/join programs: in general, steal counts should be
     * high enough to keep threads busy, but low enough to avoid
     * overhead and contention across threads.
     *
     * @return the number of steals
     */
    public long getStealCount() {
        AtomicLong sc = stealCounter;
        long count = (sc == null) ? 0L : sc.get();
        WorkQueue[] ws; WorkQueue w;
        if ((ws = workQueues) != null) {
            for (int i = 1; i < ws.length; i += 2) {
                if ((w = ws[i]) != null)
                    count += w.nsteals;
            }
        }
        return count;
    }
    /**
     * Returns an estimate of the total number of tasks currently held
     * in queues by worker threads (but not including tasks submitted
     * to the pool that have not begun executing). This value is only
     * an approximation, obtained by iterating across all threads in
     * the pool. This method may be useful for tuning task
     * granularities.
     *
     * @return the number of queued tasks
     */
    public long getQueuedTaskCount() {
        long count = 0;
        WorkQueue[] ws; WorkQueue w;
        if ((ws = workQueues) != null) {
            for (int i = 1; i < ws.length; i += 2) {
                if ((w = ws[i]) != null)
                    count += w.queueSize();
            }
        }
        return count;
    }
    /**
     * Returns an estimate of the number of tasks submitted to this
     * pool that have not yet begun executing.  This method may take
     * time proportional to the number of submissions.
     *
     * @return the number of queued submissions
     */
    public int getQueuedSubmissionCount() {
        int count = 0;
        WorkQueue[] ws; WorkQueue w;
        if ((ws = workQueues) != null) {
            for (int i = 0; i < ws.length; i += 2) {
                if ((w = ws[i]) != null)
                    count += w.queueSize();
            }
        }
        return count;
    }
    /**
     * Returns {@code true} if there are any tasks submitted to this
     * pool that have not yet begun executing.
     *
     * @return {@code true} if there are any queued submissions
     */
    public boolean hasQueuedSubmissions() {
        WorkQueue[] ws; WorkQueue w;
        if ((ws = workQueues) != null) {
            for (int i = 0; i < ws.length; i += 2) {
                if ((w = ws[i]) != null && !w.isEmpty())
                    return true;
            }
        }
        return false;
    }
    /**
     * Removes and returns the next unexecuted submission if one is
     * available.  This method may be useful in extensions to this
     * class that re-assign work in systems with multiple pools.
     *
     * @return the next submission, or {@code null} if none
     */
    protected ForkJoinTask<?> pollSubmission() {
        WorkQueue[] ws; WorkQueue w; ForkJoinTask<?> t;
        if ((ws = workQueues) != null) {
            for (int i = 0; i < ws.length; i += 2) {
                if ((w = ws[i]) != null && (t = w.poll()) != null)
                    return t;
            }
        }
        return null;
    }
    /**
     * Removes all available unexecuted submitted and forked tasks
     * from scheduling queues and adds them to the given collection,
     * without altering their execution status. These may include
     * artificially generated or wrapped tasks. This method is
     * designed to be invoked only when the pool is known to be
     * quiescent. Invocations at other times may not remove all
     * tasks. A failure encountered while attempting to add elements
     * to collection {@code c} may result in elements being in
     * neither, either or both collections when the associated
     * exception is thrown.  The behavior of this operation is
     * undefined if the specified collection is modified while the
     * operation is in progress.
     *
     * @param c the collection to transfer elements into
     * @return the number of elements transferred
     */
    protected int drainTasksTo(Collection<? super ForkJoinTask<?>> c) {
        int count = 0;
        WorkQueue[] ws; WorkQueue w; ForkJoinTask<?> t;
        if ((ws = workQueues) != null) {
            for (int i = 0; i < ws.length; ++i) {
                if ((w = ws[i]) != null) {
                    while ((t = w.poll()) != null) {
                        c.add(t);
                        ++count;
                    }
                }
            }
        }
        return count;
    }
    /**
     * Returns a string identifying this pool, as well as its state,
     * including indications of run state, parallelism level, and
     * worker and task counts.
     *
     * @return a string identifying this pool, as well as its state
     */
    public String toString() {
        // Use a single pass through workQueues to collect counts
        long qt = 0L, qs = 0L; int rc = 0;
        AtomicLong sc = stealCounter;
        long st = (sc == null) ? 0L : sc.get();
        long c = ctl;
        WorkQueue[] ws; WorkQueue w;
        if ((ws = workQueues) != null) {
            for (int i = 0; i < ws.length; ++i) {
                if ((w = ws[i]) != null) {
                    int size = w.queueSize();
                    if ((i & 1) == 0)
                        qs += size;
                    else {
                        qt += size;
                        st += w.nsteals;
                        if (w.isApparentlyUnblocked())
                            ++rc;
                    }
                }
            }
        }
        int pc = (config & SMASK);
        int tc = pc + (short)(c >>> TC_SHIFT);
        int ac = pc + (int)(c >> AC_SHIFT);
        if (ac < 0) // ignore transient negative
            ac = 0;
        int rs = runState;
        String level = ((rs & TERMINATED) != 0 ? "Terminated" :
                        (rs & STOP)       != 0 ? "Terminating" :
                        (rs & SHUTDOWN)   != 0 ? "Shutting down" :
                        "Running");
        return super.toString() +
            "[" + level +
            ", parallelism = " + pc +
            ", size = " + tc +
            ", active = " + ac +
            ", running = " + rc +
            ", steals = " + st +
            ", tasks = " + qt +
            ", submissions = " + qs +
            "]";
    }
    /**
     * Possibly initiates an orderly shutdown in which previously
     * submitted tasks are executed, but no new tasks will be
     * accepted. Invocation has no effect on execution state if this
     * is the {@link #commonPool()}, and no additional effect if
     * already shut down.  Tasks that are in the process of being
     * submitted concurrently during the course of this method may or
     * may not be rejected.
     *
     * @throws SecurityException if a security manager exists and
     *         the caller is not permitted to modify threads
     *         because it does not hold {@link
     *         java.lang.RuntimePermission}{@code ("modifyThread")}
     */
    public void shutdown() {
        checkPermission();
        tryTerminate(false, true);
    }
    /**
     * Possibly attempts to cancel and/or stop all tasks, and reject
     * all subsequently submitted tasks.  Invocation has no effect on
     * execution state if this is the {@link #commonPool()}, and no
     * additional effect if already shut down. Otherwise, tasks that
     * are in the process of being submitted or executed concurrently
     * during the course of this method may or may not be
     * rejected. This method cancels both existing and unexecuted
     * tasks, in order to permit termination in the presence of task
     * dependencies. So the method always returns an empty list
     * (unlike the case for some other Executors).
     *
     * @return an empty list
     * @throws SecurityException if a security manager exists and
     *         the caller is not permitted to modify threads
     *         because it does not hold {@link
     *         java.lang.RuntimePermission}{@code ("modifyThread")}
     */
    public List<Runnable> shutdownNow() {
        checkPermission();
        tryTerminate(true, true);
        return Collections.emptyList();
    }
    /**
     * Returns {@code true} if all tasks have completed following shut down.
     *
     * @return {@code true} if all tasks have completed following shut down
     */
    public boolean isTerminated() {
        return (runState & TERMINATED) != 0;
    }
    /**
     * Returns {@code true} if the process of termination has
     * commenced but not yet completed.  This method may be useful for
     * debugging. A return of {@code true} reported a sufficient
     * period after shutdown may indicate that submitted tasks have
     * ignored or suppressed interruption, or are waiting for I/O,
     * causing this executor not to properly terminate. (See the
     * advisory notes for class {@link ForkJoinTask} stating that
     * tasks should not normally entail blocking operations.  But if
     * they do, they must abort them on interrupt.)
     *
     * @return {@code true} if terminating but not yet terminated
     */
    public boolean isTerminating() {
        int rs = runState;
        return (rs & STOP) != 0 && (rs & TERMINATED) == 0;
    }
    /**
     * Returns {@code true} if this pool has been shut down.
     *
     * @return {@code true} if this pool has been shut down
     */
    public boolean isShutdown() {
        return (runState & SHUTDOWN) != 0;
    }
    /**
     * Blocks until all tasks have completed execution after a
     * shutdown request, or the timeout occurs, or the current thread
     * is interrupted, whichever happens first. Because the {@link
     * #commonPool()} never terminates until program shutdown, when
     * applied to the common pool, this method is equivalent to {@link
     * #awaitQuiescence(long, TimeUnit)} but always returns {@code false}.
     *
     * @param timeout the maximum time to wait
     * @param unit the time unit of the timeout argument
     * @return {@code true} if this executor terminated and
     *         {@code false} if the timeout elapsed before termination
     * @throws InterruptedException if interrupted while waiting
     */
    public boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (this == common) {
            awaitQuiescence(timeout, unit);
            return false;
        }
        long nanos = unit.toNanos(timeout);
        if (isTerminated())
            return true;
        if (nanos <= 0L)
            return false;
        long deadline = System.nanoTime() + nanos;
        synchronized (this) {
            for (;;) {
                if (isTerminated())
                    return true;
                if (nanos <= 0L)
                    return false;
                long millis = TimeUnit.NANOSECONDS.toMillis(nanos);
                wait(millis > 0L ? millis : 1L);
                nanos = deadline - System.nanoTime();
            }
        }
    }
    /**
     * If called by a ForkJoinTask operating in this pool, equivalent
     * in effect to {@link ForkJoinTask#helpQuiesce}. Otherwise,
     * waits and/or attempts to assist performing tasks until this
     * pool {@link #isQuiescent} or the indicated timeout elapses.
     *
     * @param timeout the maximum time to wait
     * @param unit the time unit of the timeout argument
     * @return {@code true} if quiescent; {@code false} if the
     * timeout elapsed.
     */
    public boolean awaitQuiescence(long timeout, TimeUnit unit) {
        long nanos = unit.toNanos(timeout);
        ForkJoinWorkerThread wt;
        Thread thread = Thread.currentThread();
        if ((thread instanceof ForkJoinWorkerThread) &&
            (wt = (ForkJoinWorkerThread)thread).pool == this) {
            helpQuiescePool(wt.workQueue);
            return true;
        }
        long startTime = System.nanoTime();
        WorkQueue[] ws;
        int r = 0, m;
        boolean found = true;
        while (!isQuiescent() && (ws = workQueues) != null &&
               (m = ws.length - 1) >= 0) {
            if (!found) {
                if ((System.nanoTime() - startTime) > nanos)
                    return false;
                Thread.yield(); // cannot block
            }
            found = false;
            for (int j = (m + 1) << 2; j >= 0; --j) {
                ForkJoinTask<?> t; WorkQueue q; int b, k;
                if ((k = r++ & m) <= m && k >= 0 && (q = ws[k]) != null &&
                    (b = q.base) - q.top < 0) {
                    found = true;
                    if ((t = q.pollAt(b)) != null)
                        t.doExec();
                    break;
                }
            }
        }
        return true;
    }
    /**
     * Waits and/or attempts to assist performing tasks indefinitely
     * until the {@link #commonPool()} {@link #isQuiescent}.
     */
    static void quiesceCommonPool() {
        common.awaitQuiescence(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
    }
    /**
     * Interface for extending managed parallelism for tasks running
     * in {@link ForkJoinPool}s.
     *
     * <p>A {@code ManagedBlocker} provides two methods.  Method
     * {@link #isReleasable} must return {@code true} if blocking is
     * not necessary. Method {@link #block} blocks the current thread
     * if necessary (perhaps internally invoking {@code isReleasable}
     * before actually blocking). These actions are performed by any
     * thread invoking {@link ForkJoinPool#managedBlock(ManagedBlocker)}.
     * The unusual methods in this API accommodate synchronizers that
     * may, but don't usually, block for long periods. Similarly, they
     * allow more efficient internal handling of cases in which
     * additional workers may be, but usually are not, needed to
     * ensure sufficient parallelism.  Toward this end,
     * implementations of method {@code isReleasable} must be amenable
     * to repeated invocation.
     *
     * <p>For example, here is a ManagedBlocker based on a
     * ReentrantLock:
     *  <pre> {@code
     * class ManagedLocker implements ManagedBlocker {
     *   final ReentrantLock lock;
     *   boolean hasLock = false;
     *   ManagedLocker(ReentrantLock lock) { this.lock = lock; }
     *   public boolean block() {
     *     if (!hasLock)
     *       lock.lock();
     *     return true;
     *   }
     *   public boolean isReleasable() {
     *     return hasLock || (hasLock = lock.tryLock());
     *   }
     * }}</pre>
     *
     * <p>Here is a class that possibly blocks waiting for an
     * item on a given queue:
     *  <pre> {@code
     * class QueueTaker<E> implements ManagedBlocker {
     *   final BlockingQueue<E> queue;
     *   volatile E item = null;
     *   QueueTaker(BlockingQueue<E> q) { this.queue = q; }
     *   public boolean block() throws InterruptedException {
     *     if (item == null)
     *       item = queue.take();
     *     return true;
     *   }
     *   public boolean isReleasable() {
     *     return item != null || (item = queue.poll()) != null;
     *   }
     *   public E getItem() { // call after pool.managedBlock completes
     *     return item;
     *   }
     * }}</pre>
     */
    public static interface ManagedBlocker {
        /**
         * Possibly blocks the current thread, for example waiting for
         * a lock or condition.
         *
         * @return {@code true} if no additional blocking is necessary
         * (i.e., if isReleasable would return true)
         * @throws InterruptedException if interrupted while waiting
         * (the method is not required to do so, but is allowed to)
         */
        boolean block() throws InterruptedException;
        /**
         * Returns {@code true} if blocking is unnecessary.
         * @return {@code true} if blocking is unnecessary
         */
        boolean isReleasable();
    }
    /**
     * Runs the given possibly blocking task.  When {@linkplain
     * ForkJoinTask#inForkJoinPool() running in a ForkJoinPool}, this
     * method possibly arranges for a spare thread to be activated if
     * necessary to ensure sufficient parallelism while the current
     * thread is blocked in {@link ManagedBlocker#block blocker.block()}.
     *
     * <p>This method repeatedly calls {@code blocker.isReleasable()} and
     * {@code blocker.block()} until either method returns {@code true}.
     * Every call to {@code blocker.block()} is preceded by a call to
     * {@code blocker.isReleasable()} that returned {@code false}.
     *
     * <p>If not running in a ForkJoinPool, this method is
     * behaviorally equivalent to
     *  <pre> {@code
     * while (!blocker.isReleasable())
     *   if (blocker.block())
     *     break;}</pre>
     *
     * If running in a ForkJoinPool, the pool may first be expanded to
     * ensure sufficient parallelism available during the call to
     * {@code blocker.block()}.
     *
     * @param blocker the blocker task
     * @throws InterruptedException if {@code blocker.block()} did so
     */
    public static void managedBlock(ManagedBlocker blocker)
        throws InterruptedException {
        ForkJoinPool p;
        ForkJoinWorkerThread wt;
        Thread t = Thread.currentThread();
        if ((t instanceof ForkJoinWorkerThread) &&
            (p = (wt = (ForkJoinWorkerThread)t).pool) != null) {
            WorkQueue w = wt.workQueue;
            while (!blocker.isReleasable()) {
                if (p.tryCompensate(w)) {
                    try {
                        do {} while (!blocker.isReleasable() &&
                                     !blocker.block());
                    } finally {
                        U.getAndAddLong(p, CTL, AC_UNIT);
                    }
                    break;
                }
            }
        }
        else {
            do {} while (!blocker.isReleasable() &&
                         !blocker.block());
        }
    }
    // AbstractExecutorService overrides.  These rely on undocumented
    // fact that ForkJoinTask.adapt returns ForkJoinTasks that also
    // implement RunnableFuture.
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new ForkJoinTask.AdaptedRunnable<T>(runnable, value);
    }
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new ForkJoinTask.AdaptedCallable<T>(callable);
    }
    // Unsafe mechanics
    private static final sun.misc.Unsafe U;
    private static final int  ABASE;
    private static final int  ASHIFT;
    private static final long CTL;
    private static final long RUNSTATE;
    private static final long STEALCOUNTER;
    private static final long PARKBLOCKER;
    private static final long QTOP;
    private static final long QLOCK;
    private static final long QSCANSTATE;
    private static final long QPARKER;
    private static final long QCURRENTSTEAL;
    private static final long QCURRENTJOIN;
    static {
        // initialize field offsets for CAS etc
        try {
            U = sun.misc.Unsafe.getUnsafe();
            Class<?> k = ForkJoinPool.class;
            CTL = U.objectFieldOffset
                (k.getDeclaredField("ctl"));
            RUNSTATE = U.objectFieldOffset
                (k.getDeclaredField("runState"));
            STEALCOUNTER = U.objectFieldOffset
                (k.getDeclaredField("stealCounter"));
            Class<?> tk = Thread.class;
            PARKBLOCKER = U.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
            Class<?> wk = WorkQueue.class;
            QTOP = U.objectFieldOffset
                (wk.getDeclaredField("top"));
            QLOCK = U.objectFieldOffset
                (wk.getDeclaredField("qlock"));
            QSCANSTATE = U.objectFieldOffset
                (wk.getDeclaredField("scanState"));
            QPARKER = U.objectFieldOffset
                (wk.getDeclaredField("parker"));
            QCURRENTSTEAL = U.objectFieldOffset
                (wk.getDeclaredField("currentSteal"));
            QCURRENTJOIN = U.objectFieldOffset
                (wk.getDeclaredField("currentJoin"));
            Class<?> ak = ForkJoinTask[].class;
            ABASE = U.arrayBaseOffset(ak);
            int scale = U.arrayIndexScale(ak);
            if ((scale & (scale - 1)) != 0)
                throw new Error("data type scale not a power of two");
            ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
        } catch (Exception e) {
            throw new Error(e);
        }
        commonMaxSpares = DEFAULT_COMMON_MAX_SPARES;
        defaultForkJoinWorkerThreadFactory =
            new DefaultForkJoinWorkerThreadFactory();
        modifyThreadPermission = new RuntimePermission("modifyThread");
        common = java.security.AccessController.doPrivileged
            (new java.security.PrivilegedAction<ForkJoinPool>() {
                public ForkJoinPool run() { return makeCommonPool(); }});
        int par = common.config & SMASK; // report 1 even if threads disabled
        commonParallelism = par > 0 ? par : 1;
    }
    /**
     * Creates and returns the common pool, respecting user settings
     * specified via system properties.
     */
    private static ForkJoinPool makeCommonPool() {
        int parallelism = -1;
        ForkJoinWorkerThreadFactory factory = null;
        UncaughtExceptionHandler handler = null;
        try // ignore exceptions in accessing/parsing properties
            String pp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.parallelism");
            String fp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.threadFactory");
            String hp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.exceptionHandler");
            if (pp != null)
                parallelism = Integer.parseInt(pp);
            if (fp != null)
                factory = ((ForkJoinWorkerThreadFactory)ClassLoader.
                           getSystemClassLoader().loadClass(fp).newInstance());
            if (hp != null)
                handler = ((UncaughtExceptionHandler)ClassLoader.
                           getSystemClassLoader().loadClass(hp).newInstance());
        } catch (Exception ignore) {
        }
        if (factory == null) {
            if (System.getSecurityManager() == null)
                factory = defaultForkJoinWorkerThreadFactory;
            else // use security-managed default
                factory = new InnocuousForkJoinWorkerThreadFactory();
        }
        if (parallelism < 0 && // default 1 less than #cores
            (parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
            parallelism = 1;
        if (parallelism > MAX_CAP)
            parallelism = MAX_CAP;
        return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
                                "ForkJoinPool.commonPool-worker-");
    }
    /**
     * Factory for innocuous worker threads
     */
    static final class InnocuousForkJoinWorkerThreadFactory
        implements ForkJoinWorkerThreadFactory {
        /**
         * An ACC to restrict permissions for the factory itself.
         * The constructed workers have no permissions set.
         */
        private static final AccessControlContext innocuousAcc;
        static {
            Permissions innocuousPerms = new Permissions();
            innocuousPerms.add(modifyThreadPermission);
            innocuousPerms.add(new RuntimePermission(
                                   "enableContextClassLoaderOverride"));
            innocuousPerms.add(new RuntimePermission(
                                   "modifyThreadGroup"));
            innocuousAcc = new AccessControlContext(new ProtectionDomain[] {
                    new ProtectionDomain(null, innocuousPerms)
                });
        }
        public final ForkJoinWorkerThread newThread(ForkJoinPool pool) {
            return (ForkJoinWorkerThread.InnocuousForkJoinWorkerThread)
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction<ForkJoinWorkerThread>() {
                    public ForkJoinWorkerThread run() {
                        return new ForkJoinWorkerThread.
                            InnocuousForkJoinWorkerThread(pool);
                    }}, innocuousAcc);
        }
    }
}

ForkJoinWorkerThread.java
public class ForkJoinWorkerThread extends Thread {
    /*
     * ForkJoinWorkerThreads are managed by ForkJoinPools and perform
     * ForkJoinTasks. For explanation, see the internal documentation
     * of class ForkJoinPool.
     *
     * This class just maintains links to its pool and WorkQueue.  The
     * pool field is set immediately upon construction, but the
     * workQueue field is not set until a call to registerWorker
     * completes. This leads to a visibility race, that is tolerated
     * by requiring that the workQueue field is only accessed by the
     * owning thread.
     *
     * Support for (non-public) subclass InnocuousForkJoinWorkerThread
     * requires that we break quite a lot of encapsulation (via Unsafe)
     * both here and in the subclass to access and set Thread fields.
     */
    final ForkJoinPool pool;                // the pool this thread works in
    final ForkJoinPool.WorkQueue workQueue; // work-stealing mechanics
    /**
     * Creates a ForkJoinWorkerThread operating in the given pool.
     *
     * @param pool the pool this thread works in
     * @throws NullPointerException if pool is null
     */
    protected ForkJoinWorkerThread(ForkJoinPool pool) {
        // Use a placeholder until a useful name can be set in registerWorker
        super("aForkJoinWorkerThread");
        this.pool = pool;
        this.workQueue = pool.registerWorker(this);
    }
    /**
     * Version for InnocuousForkJoinWorkerThread
     */
    ForkJoinWorkerThread(ForkJoinPool pool, ThreadGroup threadGroup,
                         AccessControlContext acc) {
        super(threadGroup, null, "aForkJoinWorkerThread");
        U.putOrderedObject(this, INHERITEDACCESSCONTROLCONTEXT, acc);
        eraseThreadLocals(); // clear before registering
        this.pool = pool;
        this.workQueue = pool.registerWorker(this);
    }
    /**
     * Returns the pool hosting this thread.
     *
     * @return the pool
     */
    public ForkJoinPool getPool() {
        return pool;
    }
    /**
     * Returns the unique index number of this thread in its pool.
     * The returned value ranges from zero to the maximum number of
     * threads (minus one) that may exist in the pool, and does not
     * change during the lifetime of the thread.  This method may be
     * useful for applications that track status or collect results
     * per-worker-thread rather than per-task.
     *
     * @return the index number
     */
    public int getPoolIndex() {
        return workQueue.getPoolIndex();
    }
    /**
     * Initializes internal state after construction but before
     * processing any tasks. If you override this method, you must
     * invoke {@code super.onStart()} at the beginning of the method.
     * Initialization requires care: Most fields must have legal
     * default values, to ensure that attempted accesses from other
     * threads work correctly even before this thread starts
     * processing tasks.
     */
    protected void onStart() {
    }
    /**
     * Performs cleanup associated with termination of this worker
     * thread.  If you override this method, you must invoke
     * {@code super.onTermination} at the end of the overridden method.
     *
     * @param exception the exception causing this thread to abort due
     * to an unrecoverable error, or {@code null} if completed normally
     */
    protected void onTermination(Throwable exception) {
    }
    /**
     * This method is required to be public, but should never be
     * called explicitly. It performs the main run loop to execute
     * {@link ForkJoinTask}s.
     */
    public void run() {
        if (workQueue.array == null) { // only run once
            Throwable exception = null;
            try {
                onStart();
                pool.runWorker(workQueue);
            } catch (Throwable ex) {
                exception = ex;
            } finally {
                try {
                    onTermination(exception);
                } catch (Throwable ex) {
                    if (exception == null)
                        exception = ex;
                } finally {
                    pool.deregisterWorker(this, exception);
                }
            }
        }
    }
    /**
     * Erases ThreadLocals by nulling out Thread maps.
     */
    final void eraseThreadLocals() {
        U.putObject(this, THREADLOCALS, null);
        U.putObject(this, INHERITABLETHREADLOCALS, null);
    }
    /**
     * Non-public hook method for InnocuousForkJoinWorkerThread
     */
    void afterTopLevelExec() {
    }
    // Set up to allow setting thread fields in constructor
    private static final sun.misc.Unsafe U;
    private static final long THREADLOCALS;
    private static final long INHERITABLETHREADLOCALS;
    private static final long INHERITEDACCESSCONTROLCONTEXT;
    static {
        try {
            U = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            THREADLOCALS = U.objectFieldOffset
                (tk.getDeclaredField("threadLocals"));
            INHERITABLETHREADLOCALS = U.objectFieldOffset
                (tk.getDeclaredField("inheritableThreadLocals"));
            INHERITEDACCESSCONTROLCONTEXT = U.objectFieldOffset
                (tk.getDeclaredField("inheritedAccessControlContext"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
    /**
     * A worker thread that has no permissions, is not a member of any
     * user-defined ThreadGroup, and erases all ThreadLocals after
     * running each top-level task.
     */
    static final class InnocuousForkJoinWorkerThread extends ForkJoinWorkerThread {
        /** The ThreadGroup for all InnocuousForkJoinWorkerThreads */
        private static final ThreadGroup innocuousThreadGroup =
            createThreadGroup();
        /** An AccessControlContext supporting no privileges */
        private static final AccessControlContext INNOCUOUS_ACC =
            new AccessControlContext(
                new ProtectionDomain[] {
                    new ProtectionDomain(null, null)
                });
        InnocuousForkJoinWorkerThread(ForkJoinPool pool) {
            super(pool, innocuousThreadGroup, INNOCUOUS_ACC);
        }
        @Override // to erase ThreadLocals
        void afterTopLevelExec() {
            eraseThreadLocals();
        }
        @Override // to always report system loader
        public ClassLoader getContextClassLoader() {
            return ClassLoader.getSystemClassLoader();
        }
        @Override // to silently fail
        public void setUncaughtExceptionHandler(UncaughtExceptionHandler x) { }
        @Override // paranoically
        public void setContextClassLoader(ClassLoader cl) {
            throw new SecurityException("setContextClassLoader");
        }
        /**
         * Returns a new group with the system ThreadGroup (the
         * topmost, parent-less group) as parent.  Uses Unsafe to
         * traverse Thread.group and ThreadGroup.parent fields.
         */
        private static ThreadGroup createThreadGroup() {
            try {
                sun.misc.Unsafe u = sun.misc.Unsafe.getUnsafe();
                Class<?> tk = Thread.class;
                Class<?> gk = ThreadGroup.class;
                long tg = u.objectFieldOffset(tk.getDeclaredField("group"));
                long gp = u.objectFieldOffset(gk.getDeclaredField("parent"));
                ThreadGroup group = (ThreadGroup)
                    u.getObject(Thread.currentThread(), tg);
                while (group != null) {
                    ThreadGroup parent = (ThreadGroup)u.getObject(group, gp);
                    if (parent == null)
                        return new ThreadGroup(group,
                                               "InnocuousForkJoinWorkerThreadGroup");
                    group = parent;
                }
            } catch (Exception e) {
                throw new Error(e);
            }
            // fall through if null as cannot-happen safeguard
            throw new Error("Cannot create ThreadGroup");
        }
    }
}

ForkJoinTask.java
public abstract class ForkJoinTask<V> implements Future<V>, Serializable {
    /*
     * See the internal documentation of class ForkJoinPool for a
     * general implementation overview.  ForkJoinTasks are mainly
     * responsible for maintaining their "status" field amidst relays
     * to methods in ForkJoinWorkerThread and ForkJoinPool.
     *
     * The methods of this class are more-or-less layered into
     * (1) basic status maintenance
     * (2) execution and awaiting completion
     * (3) user-level methods that additionally report results.
     * This is sometimes hard to see because this file orders exported
     * methods in a way that flows well in javadocs.
     */
    /*
     * The status field holds run control status bits packed into a
     * single int to minimize footprint and to ensure atomicity (via
     * CAS).  Status is initially zero, and takes on nonnegative
     * values until completed, upon which status (anded with
     * DONE_MASK) holds value NORMAL, CANCELLED, or EXCEPTIONAL. Tasks
     * undergoing blocking waits by other threads have the SIGNAL bit
     * set.  Completion of a stolen task with SIGNAL set awakens any
     * waiters via notifyAll. Even though suboptimal for some
     * purposes, we use basic builtin wait/notify to take advantage of
     * "monitor inflation" in JVMs that we would otherwise need to
     * emulate to avoid adding further per-task bookkeeping overhead.
     * We want these monitors to be "fat", i.e., not use biasing or
     * thin-lock techniques, so use some odd coding idioms that tend
     * to avoid them, mainly by arranging that every synchronized
     * block performs a wait, notifyAll or both.
     *
     * These control bits occupy only (some of) the upper half (16
     * bits) of status field. The lower bits are used for user-defined
     * tags.
     */
    /** The run status of this task */
    volatile int status; // accessed directly by pool and workers
    static final int DONE_MASK   = 0xf0000000;  // mask out non-completion bits
    static final int NORMAL      = 0xf0000000;  // must be negative
    static final int CANCELLED   = 0xc0000000;  // must be < NORMAL
    static final int EXCEPTIONAL = 0x80000000;  // must be < CANCELLED
    static final int SIGNAL      = 0x00010000;  // must be >= 1 << 16
    static final int SMASK       = 0x0000ffff;  // short bits for tags
    /**
     * Marks completion and wakes up threads waiting to join this
     * task.
     *
     * @param completion one of NORMAL, CANCELLED, EXCEPTIONAL
     * @return completion status on exit
     */
    private int setCompletion(int completion) {
        for (int s;;) {
            if ((s = status) < 0)
                return s;
            if (U.compareAndSwapInt(this, STATUS, s, s | completion)) {
                if ((s >>> 16) != 0)
                    synchronized (this) { notifyAll(); }
                return completion;
            }
        }
    }
    /**
     * Primary execution method for stolen tasks. Unless done, calls
     * exec and records status if completed, but doesn't wait for
     * completion otherwise.
     *
     * @return status on exit from this method
     */
    final int doExec() {
        int s; boolean completed;
        if ((s = status) >= 0) {
            try {
                completed = exec();
            } catch (Throwable rex) {
                return setExceptionalCompletion(rex);
            }
            if (completed)
                s = setCompletion(NORMAL);
        }
        return s;
    }
    /**
     * If not done, sets SIGNAL status and performs Object.wait(timeout).
     * This task may or may not be done on exit. Ignores interrupts.
     *
     * @param timeout using Object.wait conventions.
     */
    final void internalWait(long timeout) {
        int s;
        if ((s = status) >= 0 && // force completer to issue notify
            U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
            synchronized (this) {
                if (status >= 0)
                    try { wait(timeout); } catch (InterruptedException ie) { }
                else
                    notifyAll();
            }
        }
    }
    /**
     * Blocks a non-worker-thread until completion.
     * @return status upon completion
     */
    private int externalAwaitDone() {
        int s = ((this instanceof CountedCompleter) ? // try helping
                 ForkJoinPool.common.externalHelpComplete(
                     (CountedCompleter<?>)this, 0) :
                 ForkJoinPool.common.tryExternalUnpush(this) ? doExec() : 0);
        if (s >= 0 && (s = status) >= 0) {
            boolean interrupted = false;
            do {
                if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
                    synchronized (this) {
                        if (status >= 0) {
                            try {
                                wait(0L);
                            } catch (InterruptedException ie) {
                                interrupted = true;
                            }
                        }
                        else
                            notifyAll();
                    }
                }
            } while ((s = status) >= 0);
            if (interrupted)
                Thread.currentThread().interrupt();
        }
        return s;
    }
    /**
     * Blocks a non-worker-thread until completion or interruption.
     */
    private int externalInterruptibleAwaitDone() throws InterruptedException {
        int s;
        if (Thread.interrupted())
            throw new InterruptedException();
        if ((s = status) >= 0 &&
            (s = ((this instanceof CountedCompleter) ?
                  ForkJoinPool.common.externalHelpComplete(
                      (CountedCompleter<?>)this, 0) :
                  ForkJoinPool.common.tryExternalUnpush(this) ? doExec() :
                  0)) >= 0) {
            while ((s = status) >= 0) {
                if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
                    synchronized (this) {
                        if (status >= 0)
                            wait(0L);
                        else
                            notifyAll();
                    }
                }
            }
        }
        return s;
    }
    /**
     * Implementation for join, get, quietlyJoin. Directly handles
     * only cases of already-completed, external wait, and
     * unfork+exec.  Others are relayed to ForkJoinPool.awaitJoin.
     *
     * @return status upon completion
     */
    private int doJoin() {
        int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
        return (s = status) < 0 ? s :
            ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            (w = (wt = (ForkJoinWorkerThread)t).workQueue).
            tryUnpush(this) && (s = doExec()) < 0 ? s :
            wt.pool.awaitJoin(w, this, 0L) :
            externalAwaitDone();
    }
    /**
     * Implementation for invoke, quietlyInvoke.
     *
     * @return status upon completion
     */
    private int doInvoke() {
        int s; Thread t; ForkJoinWorkerThread wt;
        return (s = doExec()) < 0 ? s :
            ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            (wt = (ForkJoinWorkerThread)t).pool.
            awaitJoin(wt.workQueue, this, 0L) :
            externalAwaitDone();
    }
    // Exception table support
    /**
     * Table of exceptions thrown by tasks, to enable reporting by
     * callers. Because exceptions are rare, we don't directly keep
     * them with task objects, but instead use a weak ref table.  Note
     * that cancellation exceptions don't appear in the table, but are
     * instead recorded as status values.
     *
     * Note: These statics are initialized below in static block.
     */
    private static final ExceptionNode[] exceptionTable;
    private static final ReentrantLock exceptionTableLock;
    private static final ReferenceQueue<Object> exceptionTableRefQueue;
    /**
     * Fixed capacity for exceptionTable.
     */
    private static final int EXCEPTION_MAP_CAPACITY = 32;
    /**
     * Key-value nodes for exception table.  The chained hash table
     * uses identity comparisons, full locking, and weak references
     * for keys. The table has a fixed capacity because it only
     * maintains task exceptions long enough for joiners to access
     * them, so should never become very large for sustained
     * periods. However, since we do not know when the last joiner
     * completes, we must use weak references and expunge them. We do
     * so on each operation (hence full locking). Also, some thread in
     * any ForkJoinPool will call helpExpungeStaleExceptions when its
     * pool becomes isQuiescent.
     */
    static final class ExceptionNode extends WeakReference<ForkJoinTask<?>> {
        final Throwable ex;
        ExceptionNode next;
        final long thrower // use id not ref to avoid weak cycles
        final int hashCode // store task hashCode before weak ref disappears
        ExceptionNode(ForkJoinTask<?> task, Throwable ex, ExceptionNode next) {
            super(task, exceptionTableRefQueue);
            this.ex = ex;
            this.next = next;
            this.thrower = Thread.currentThread().getId();
            this.hashCode = System.identityHashCode(task);
        }
    }
    /**
     * Records exception and sets status.
     *
     * @return status on exit
     */
    final int recordExceptionalCompletion(Throwable ex) {
        int s;
        if ((s = status) >= 0) {
            int h = System.identityHashCode(this);
            final ReentrantLock lock = exceptionTableLock;
            lock.lock();
            try {
                expungeStaleExceptions();
                ExceptionNode[] t = exceptionTable;
                int i = h & (t.length - 1);
                for (ExceptionNode e = t[i]; ; e = e.next) {
                    if (e == null) {
                        t[i] = new ExceptionNode(this, ex, t[i]);
                        break;
                    }
                    if (e.get() == this) // already present
                        break;
                }
            } finally {
                lock.unlock();
            }
            s = setCompletion(EXCEPTIONAL);
        }
        return s;
    }
    /**
     * Records exception and possibly propagates.
     *
     * @return status on exit
     */
    private int setExceptionalCompletion(Throwable ex) {
        int s = recordExceptionalCompletion(ex);
        if ((s & DONE_MASK) == EXCEPTIONAL)
            internalPropagateException(ex);
        return s;
    }
    /**
     * Hook for exception propagation support for tasks with completers.
     */
    void internalPropagateException(Throwable ex) {
    }
    /**
     * Cancels, ignoring any exceptions thrown by cancel. Used during
     * worker and pool shutdown. Cancel is spec'ed not to throw any
     * exceptions, but if it does anyway, we have no recourse during
     * shutdown, so guard against this case.
     */
    static final void cancelIgnoringExceptions(ForkJoinTask<?> t) {
        if (t != null && t.status >= 0) {
            try {
                t.cancel(false);
            } catch (Throwable ignore) {
            }
        }
    }
    /**
     * Removes exception node and clears status.
     */
    private void clearExceptionalCompletion() {
        int h = System.identityHashCode(this);
        final ReentrantLock lock = exceptionTableLock;
        lock.lock();
        try {
            ExceptionNode[] t = exceptionTable;
            int i = h & (t.length - 1);
            ExceptionNode e = t[i];
            ExceptionNode pred = null;
            while (e != null) {
                ExceptionNode next = e.next;
                if (e.get() == this) {
                    if (pred == null)
                        t[i] = next;
                    else
                        pred.next = next;
                    break;
                }
                pred = e;
                e = next;
            }
            expungeStaleExceptions();
            status = 0;
        } finally {
            lock.unlock();
        }
    }
    /**
     * Returns a rethrowable exception for the given task, if
     * available. To provide accurate stack traces, if the exception
     * was not thrown by the current thread, we try to create a new
     * exception of the same type as the one thrown, but with the
     * recorded exception as its cause. If there is no such
     * constructor, we instead try to use a no-arg constructor,
     * followed by initCause, to the same effect. If none of these
     * apply, or any fail due to other exceptions, we return the
     * recorded exception, which is still correct, although it may
     * contain a misleading stack trace.
     *
     * @return the exception, or null if none
     */
    private Throwable getThrowableException() {
        if ((status & DONE_MASK) != EXCEPTIONAL)
            return null;
        int h = System.identityHashCode(this);
        ExceptionNode e;
        final ReentrantLock lock = exceptionTableLock;
        lock.lock();
        try {
            expungeStaleExceptions();
            ExceptionNode[] t = exceptionTable;
            e = t[h & (t.length - 1)];
            while (e != null && e.get() != this)
                e = e.next;
        } finally {
            lock.unlock();
        }
        Throwable ex;
        if (e == null || (ex = e.ex) == null)
            return null;
        if (e.thrower != Thread.currentThread().getId()) {
            Class<? extends Throwable> ec = ex.getClass();
            try {
                Constructor<?> noArgCtor = null;
                Constructor<?>[] cs = ec.getConstructors();// public ctors only
                for (int i = 0; i < cs.length; ++i) {
                    Constructor<?> c = cs[i];
                    Class<?>[] ps = c.getParameterTypes();
                    if (ps.length == 0)
                        noArgCtor = c;
                    else if (ps.length == 1 && ps[0] == Throwable.class) {
                        Throwable wx = (Throwable)c.newInstance(ex);
                        return (wx == null) ? ex : wx;
                    }
                }
                if (noArgCtor != null) {
                    Throwable wx = (Throwable)(noArgCtor.newInstance());
                    if (wx != null) {
                        wx.initCause(ex);
                        return wx;
                    }
                }
            } catch (Exception ignore) {
            }
        }
        return ex;
    }
    /**
     * Poll stale refs and remove them. Call only while holding lock.
     */
    private static void expungeStaleExceptions() {
        for (Object x; (x = exceptionTableRefQueue.poll()) != null;) {
            if (x instanceof ExceptionNode) {
                int hashCode = ((ExceptionNode)x).hashCode;
                ExceptionNode[] t = exceptionTable;
                int i = hashCode & (t.length - 1);
                ExceptionNode e = t[i];
                ExceptionNode pred = null;
                while (e != null) {
                    ExceptionNode next = e.next;
                    if (e == x) {
                        if (pred == null)
                            t[i] = next;
                        else
                            pred.next = next;
                        break;
                    }
                    pred = e;
                    e = next;
                }
            }
        }
    }
    /**
     * If lock is available, poll stale refs and remove them.
     * Called from ForkJoinPool when pools become quiescent.
     */
    static final void helpExpungeStaleExceptions() {
        final ReentrantLock lock = exceptionTableLock;
        if (lock.tryLock()) {
            try {
                expungeStaleExceptions();
            } finally {
                lock.unlock();
            }
        }
    }
    /**
     * A version of "sneaky throw" to relay exceptions
     */
    static void rethrow(Throwable ex) {
        if (ex != null)
            ForkJoinTask.<RuntimeException>uncheckedThrow(ex);
    }
    /**
     * The sneaky part of sneaky throw, relying on generics
     * limitations to evade compiler complaints about rethrowing
     * unchecked exceptions
     */
    @SuppressWarnings("unchecked") static <T extends Throwable>
        void uncheckedThrow(Throwable t) throws T {
        throw (T)t; // rely on vacuous cast
    }
    /**
     * Throws exception, if any, associated with the given status.
     */
    private void reportException(int s) {
        if (s == CANCELLED)
            throw new CancellationException();
        if (s == EXCEPTIONAL)
            rethrow(getThrowableException());
    }
    // public methods
    /**
     * Arranges to asynchronously execute this task in the pool the
     * current task is running in, if applicable, or using the {@link
     * ForkJoinPool#commonPool()} if not {@link #inForkJoinPool}.  While
     * it is not necessarily enforced, it is a usage error to fork a
     * task more than once unless it has completed and been
     * reinitialized.  Subsequent modifications to the state of this
     * task or any data it operates on are not necessarily
     * consistently observable by any thread other than the one
     * executing it unless preceded by a call to {@link #join} or
     * related methods, or a call to {@link #isDone} returning {@code
     * true}.
     *
     * @return {@code this}, to simplify usage
     */
    public final ForkJoinTask<V> fork() {
        Thread t;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            ((ForkJoinWorkerThread)t).workQueue.push(this);
        else
            ForkJoinPool.common.externalPush(this);
        return this;
    }
    /**
     * Returns the result of the computation when it {@link #isDone is
     * done}.  This method differs from {@link #get()} in that
     * abnormal completion results in {@code RuntimeException} or
     * {@code Error}, not {@code ExecutionException}, and that
     * interrupts of the calling thread do <em>not</em> cause the
     * method to abruptly return by throwing {@code
     * InterruptedException}.
     *
     * @return the computed result
     */
    public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }
    /**
     * Commences performing this task, awaits its completion if
     * necessary, and returns its result, or throws an (unchecked)
     * {@code RuntimeException} or {@code Error} if the underlying
     * computation did so.
     *
     * @return the computed result
     */
    public final V invoke() {
        int s;
        if ((s = doInvoke() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }
    /**
     * Forks the given tasks, returning when {@code isDone} holds for
     * each task or an (unchecked) exception is encountered, in which
     * case the exception is rethrown. If more than one task
     * encounters an exception, then this method throws any one of
     * these exceptions. If any task encounters an exception, the
     * other may be cancelled. However, the execution status of
     * individual tasks is not guaranteed upon exceptional return. The
     * status of each task may be obtained using {@link
     * #getException()} and related methods to check if they have been
     * cancelled, completed normally or exceptionally, or left
     * unprocessed.
     *
     * @param t1 the first task
     * @param t2 the second task
     * @throws NullPointerException if any task is null
     */
    public static void invokeAll(ForkJoinTask<?> t1, ForkJoinTask<?> t2) {
        int s1, s2;
        t2.fork();
        if ((s1 = t1.doInvoke() & DONE_MASK) != NORMAL)
            t1.reportException(s1);
        if ((s2 = t2.doJoin() & DONE_MASK) != NORMAL)
            t2.reportException(s2);
    }
    /**
     * Forks the given tasks, returning when {@code isDone} holds for
     * each task or an (unchecked) exception is encountered, in which
     * case the exception is rethrown. If more than one task
     * encounters an exception, then this method throws any one of
     * these exceptions. If any task encounters an exception, others
     * may be cancelled. However, the execution status of individual
     * tasks is not guaranteed upon exceptional return. The status of
     * each task may be obtained using {@link #getException()} and
     * related methods to check if they have been cancelled, completed
     * normally or exceptionally, or left unprocessed.
     *
     * @param tasks the tasks
     * @throws NullPointerException if any task is null
     */
    public static void invokeAll(ForkJoinTask<?>... tasks) {
        Throwable ex = null;
        int last = tasks.length - 1;
        for (int i = last; i >= 0; --i) {
            ForkJoinTask<?> t = tasks[i];
            if (t == null) {
                if (ex == null)
                    ex = new NullPointerException();
            }
            else if (i != 0)
                t.fork();
            else if (t.doInvoke() < NORMAL && ex == null)
                ex = t.getException();
        }
        for (int i = 1; i <= last; ++i) {
            ForkJoinTask<?> t = tasks[i];
            if (t != null) {
                if (ex != null)
                    t.cancel(false);
                else if (t.doJoin() < NORMAL)
                    ex = t.getException();
            }
        }
        if (ex != null)
            rethrow(ex);
    }
    /**
     * Forks all tasks in the specified collection, returning when
     * {@code isDone} holds for each task or an (unchecked) exception
     * is encountered, in which case the exception is rethrown. If
     * more than one task encounters an exception, then this method
     * throws any one of these exceptions. If any task encounters an
     * exception, others may be cancelled. However, the execution
     * status of individual tasks is not guaranteed upon exceptional
     * return. The status of each task may be obtained using {@link
     * #getException()} and related methods to check if they have been
     * cancelled, completed normally or exceptionally, or left
     * unprocessed.
     *
     * @param tasks the collection of tasks
     * @param <T> the type of the values returned from the tasks
     * @return the tasks argument, to simplify usage
     * @throws NullPointerException if tasks or any element are null
     */
    public static <T extends ForkJoinTask<?>> Collection<T> invokeAll(Collection<T> tasks) {
        if (!(tasks instanceof RandomAccess) || !(tasks instanceof List<?>)) {
            invokeAll(tasks.toArray(new ForkJoinTask<?>[tasks.size()]));
            return tasks;
        }
        @SuppressWarnings("unchecked")
        List<? extends ForkJoinTask<?>> ts =
            (List<? extends ForkJoinTask<?>>) tasks;
        Throwable ex = null;
        int last = ts.size() - 1;
        for (int i = last; i >= 0; --i) {
            ForkJoinTask<?> t = ts.get(i);
            if (t == null) {
                if (ex == null)
                    ex = new NullPointerException();
            }
            else if (i != 0)
                t.fork();
            else if (t.doInvoke() < NORMAL && ex == null)
                ex = t.getException();
        }
        for (int i = 1; i <= last; ++i) {
            ForkJoinTask<?> t = ts.get(i);
            if (t != null) {
                if (ex != null)
                    t.cancel(false);
                else if (t.doJoin() < NORMAL)
                    ex = t.getException();
            }
        }
        if (ex != null)
            rethrow(ex);
        return tasks;
    }
    /**
     * Attempts to cancel execution of this task. This attempt will
     * fail if the task has already completed or could not be
     * cancelled for some other reason. If successful, and this task
     * has not started when {@code cancel} is called, execution of
     * this task is suppressed. After this method returns
     * successfully, unless there is an intervening call to {@link
     * #reinitialize}, subsequent calls to {@link #isCancelled},
     * {@link #isDone}, and {@code cancel} will return {@code true}
     * and calls to {@link #join} and related methods will result in
     * {@code CancellationException}.
     *
     * <p>This method may be overridden in subclasses, but if so, must
     * still ensure that these properties hold. In particular, the
     * {@code cancel} method itself must not throw exceptions.
     *
     * <p>This method is designed to be invoked by <em>other</em>
     * tasks. To terminate the current task, you can just return or
     * throw an unchecked exception from its computation method, or
     * invoke {@link #completeExceptionally(Throwable)}.
     *
     * @param mayInterruptIfRunning this value has no effect in the
     * default implementation because interrupts are not used to
     * control cancellation.
     *
     * @return {@code true} if this task is now cancelled
     */
    public boolean cancel(boolean mayInterruptIfRunning) {
        return (setCompletion(CANCELLED) & DONE_MASK) == CANCELLED;
    }
    public final boolean isDone() {
        return status < 0;
    }
    public final boolean isCancelled() {
        return (status & DONE_MASK) == CANCELLED;
    }
    /**
     * Returns {@code true} if this task threw an exception or was cancelled.
     *
     * @return {@code true} if this task threw an exception or was cancelled
     */
    public final boolean isCompletedAbnormally() {
        return status < NORMAL;
    }
    /**
     * Returns {@code true} if this task completed without throwing an
     * exception and was not cancelled.
     *
     * @return {@code true} if this task completed without throwing an
     * exception and was not cancelled
     */
    public final boolean isCompletedNormally() {
        return (status & DONE_MASK) == NORMAL;
    }
    /**
     * Returns the exception thrown by the base computation, or a
     * {@code CancellationException} if cancelled, or {@code null} if
     * none or if the method has not yet completed.
     *
     * @return the exception, or {@code null} if none
     */
    public final Throwable getException() {
        int s = status & DONE_MASK;
        return ((s >= NORMAL)    ? null :
                (s == CANCELLED) ? new CancellationException() :
                getThrowableException());
    }
    /**
     * Completes this task abnormally, and if not already aborted or
     * cancelled, causes it to throw the given exception upon
     * {@code join} and related operations. This method may be used
     * to induce exceptions in asynchronous tasks, or to force
     * completion of tasks that would not otherwise complete.  Its use
     * in other situations is discouraged.  This method is
     * overridable, but overridden versions must invoke {@code super}
     * implementation to maintain guarantees.
     *
     * @param ex the exception to throw. If this exception is not a
     * {@code RuntimeException} or {@code Error}, the actual exception
     * thrown will be a {@code RuntimeException} with cause {@code ex}.
     */
    public void completeExceptionally(Throwable ex) {
        setExceptionalCompletion((ex instanceof RuntimeException) ||
                                 (ex instanceof Error) ? ex :
                                 new RuntimeException(ex));
    }
    /**
     * Completes this task, and if not already aborted or cancelled,
     * returning the given value as the result of subsequent
     * invocations of {@code join} and related operations. This method
     * may be used to provide results for asynchronous tasks, or to
     * provide alternative handling for tasks that would not otherwise
     * complete normally. Its use in other situations is
     * discouraged. This method is overridable, but overridden
     * versions must invoke {@code super} implementation to maintain
     * guarantees.
     *
     * @param value the result value for this task
     */
    public void complete(V value) {
        try {
            setRawResult(value);
        } catch (Throwable rex) {
            setExceptionalCompletion(rex);
            return;
        }
        setCompletion(NORMAL);
    }
    /**
     * Completes this task normally without setting a value. The most
     * recent value established by {@link #setRawResult} (or {@code
     * null} by default) will be returned as the result of subsequent
     * invocations of {@code join} and related operations.
     *
     * @since 1.8
     */
    public final void quietlyComplete() {
        setCompletion(NORMAL);
    }
    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     *
     * @return the computed result
     * @throws CancellationException if the computation was cancelled
     * @throws ExecutionException if the computation threw an
     * exception
     * @throws InterruptedException if the current thread is not a
     * member of a ForkJoinPool and was interrupted while waiting
     */
    public final V get() throws InterruptedException, ExecutionException {
        int s = (Thread.currentThread() instanceof ForkJoinWorkerThread) ?
            doJoin() : externalInterruptibleAwaitDone();
        Throwable ex;
        if ((s &= DONE_MASK) == CANCELLED)
            throw new CancellationException();
        if (s == EXCEPTIONAL && (ex = getThrowableException()) != null)
            throw new ExecutionException(ex);
        return getRawResult();
    }
    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result, if available.
     *
     * @param timeout the maximum time to wait
     * @param unit the time unit of the timeout argument
     * @return the computed result
     * @throws CancellationException if the computation was cancelled
     * @throws ExecutionException if the computation threw an
     * exception
     * @throws InterruptedException if the current thread is not a
     * member of a ForkJoinPool and was interrupted while waiting
     * @throws TimeoutException if the wait timed out
     */
    public final V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        int s;
        long nanos = unit.toNanos(timeout);
        if (Thread.interrupted())
            throw new InterruptedException();
        if ((s = status) >= 0 && nanos > 0L) {
            long d = System.nanoTime() + nanos;
            long deadline = (d == 0L) ? 1L : d; // avoid 0
            Thread t = Thread.currentThread();
            if (t instanceof ForkJoinWorkerThread) {
                ForkJoinWorkerThread wt = (ForkJoinWorkerThread)t;
                s = wt.pool.awaitJoin(wt.workQueue, this, deadline);
            }
            else if ((s = ((this instanceof CountedCompleter) ?
                           ForkJoinPool.common.externalHelpComplete(
                               (CountedCompleter<?>)this, 0) :
                           ForkJoinPool.common.tryExternalUnpush(this) ?
                           doExec() : 0)) >= 0) {
                long ns, ms; // measure in nanosecs, but wait in millisecs
                while ((s = status) >= 0 &&
                       (ns = deadline - System.nanoTime()) > 0L) {
                    if ((ms = TimeUnit.NANOSECONDS.toMillis(ns)) > 0L &&
                        U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
                        synchronized (this) {
                            if (status >= 0)
                                wait(ms); // OK to throw InterruptedException
                            else
                                notifyAll();
                        }
                    }
                }
            }
        }
        if (s >= 0)
            s = status;
        if ((s &= DONE_MASK) != NORMAL) {
            Throwable ex;
            if (s == CANCELLED)
                throw new CancellationException();
            if (s != EXCEPTIONAL)
                throw new TimeoutException();
            if ((ex = getThrowableException()) != null)
                throw new ExecutionException(ex);
        }
        return getRawResult();
    }
    /**
     * Joins this task, without returning its result or throwing its
     * exception. This method may be useful when processing
     * collections of tasks when some have been cancelled or otherwise
     * known to have aborted.
     */
    public final void quietlyJoin() {
        doJoin();
    }
    /**
     * Commences performing this task and awaits its completion if
     * necessary, without returning its result or throwing its
     * exception.
     */
    public final void quietlyInvoke() {
        doInvoke();
    }
    /**
     * Possibly executes tasks until the pool hosting the current task
     * {@link ForkJoinPool#isQuiescent is quiescent}. This method may
     * be of use in designs in which many tasks are forked, but none
     * are explicitly joined, instead executing them until all are
     * processed.
     */
    public static void helpQuiesce() {
        Thread t;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) {
            ForkJoinWorkerThread wt = (ForkJoinWorkerThread)t;
            wt.pool.helpQuiescePool(wt.workQueue);
        }
        else
            ForkJoinPool.quiesceCommonPool();
    }
    /**
     * Resets the internal bookkeeping state of this task, allowing a
     * subsequent {@code fork}. This method allows repeated reuse of
     * this task, but only if reuse occurs when this task has either
     * never been forked, or has been forked, then completed and all
     * outstanding joins of this task have also completed. Effects
     * under any other usage conditions are not guaranteed.
     * This method may be useful when executing
     * pre-constructed trees of subtasks in loops.
     *
     * <p>Upon completion of this method, {@code isDone()} reports
     * {@code false}, and {@code getException()} reports {@code
     * null}. However, the value returned by {@code getRawResult} is
     * unaffected. To clear this value, you can invoke {@code
     * setRawResult(null)}.
     */
    public void reinitialize() {
        if ((status & DONE_MASK) == EXCEPTIONAL)
            clearExceptionalCompletion();
        else
            status = 0;
    }
    /**
     * Returns the pool hosting the current task execution, or null
     * if this task is executing outside of any ForkJoinPool.
     *
     * @see #inForkJoinPool
     * @return the pool, or {@code null} if none
     */
    public static ForkJoinPool getPool() {
        Thread t = Thread.currentThread();
        return (t instanceof ForkJoinWorkerThread) ?
            ((ForkJoinWorkerThread) t).pool : null;
    }
    /**
     * Returns {@code true} if the current thread is a {@link
     * ForkJoinWorkerThread} executing as a ForkJoinPool computation.
     *
     * @return {@code true} if the current thread is a {@link
     * ForkJoinWorkerThread} executing as a ForkJoinPool computation,
     * or {@code false} otherwise
     */
    public static boolean inForkJoinPool() {
        return Thread.currentThread() instanceof ForkJoinWorkerThread;
    }
    /**
     * Tries to unschedule this task for execution. This method will
     * typically (but is not guaranteed to) succeed if this task is
     * the most recently forked task by the current thread, and has
     * not commenced executing in another thread.  This method may be
     * useful when arranging alternative local processing of tasks
     * that could have been, but were not, stolen.
     *
     * @return {@code true} if unforked
     */
    public boolean tryUnfork() {
        Thread t;
        return (((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
                ((ForkJoinWorkerThread)t).workQueue.tryUnpush(this) :
                ForkJoinPool.common.tryExternalUnpush(this));
    }
    /**
     * Returns an estimate of the number of tasks that have been
     * forked by the current worker thread but not yet executed. This
     * value may be useful for heuristic decisions about whether to
     * fork other tasks.
     *
     * @return the number of tasks
     */
    public static int getQueuedTaskCount() {
        Thread t; ForkJoinPool.WorkQueue q;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            q = ((ForkJoinWorkerThread)t).workQueue;
        else
            q = ForkJoinPool.commonSubmitterQueue();
        return (q == null) ? 0 : q.queueSize();
    }
    /**
     * Returns an estimate of how many more locally queued tasks are
     * held by the current worker thread than there are other worker
     * threads that might steal them, or zero if this thread is not
     * operating in a ForkJoinPool. This value may be useful for
     * heuristic decisions about whether to fork other tasks. In many
     * usages of ForkJoinTasks, at steady state, each worker should
     * aim to maintain a small constant surplus (for example, 3) of
     * tasks, and to process computations locally if this threshold is
     * exceeded.
     *
     * @return the surplus number of tasks, which may be negative
     */
    public static int getSurplusQueuedTaskCount() {
        return ForkJoinPool.getSurplusQueuedTaskCount();
    }
    // Extension methods
    /**
     * Returns the result that would be returned by {@link #join}, even
     * if this task completed abnormally, or {@code null} if this task
     * is not known to have been completed.  This method is designed
     * to aid debugging, as well as to support extensions. Its use in
     * any other context is discouraged.
     *
     * @return the result, or {@code null} if not completed
     */
    public abstract V getRawResult();
    /**
     * Forces the given value to be returned as a result.  This method
     * is designed to support extensions, and should not in general be
     * called otherwise.
     *
     * @param value the value
     */
    protected abstract void setRawResult(V value);
    /**
     * Immediately performs the base action of this task and returns
     * true if, upon return from this method, this task is guaranteed
     * to have completed normally. This method may return false
     * otherwise, to indicate that this task is not necessarily
     * complete (or is not known to be complete), for example in
     * asynchronous actions that require explicit invocations of
     * completion methods. This method may also throw an (unchecked)
     * exception to indicate abnormal exit. This method is designed to
     * support extensions, and should not in general be called
     * otherwise.
     *
     * @return {@code true} if this task is known to have completed normally
     */
    protected abstract boolean exec();
    /**
     * Returns, but does not unschedule or execute, a task queued by
     * the current thread but not yet executed, if one is immediately
     * available. There is no guarantee that this task will actually
     * be polled or executed next. Conversely, this method may return
     * null even if a task exists but cannot be accessed without
     * contention with other threads.  This method is designed
     * primarily to support extensions, and is unlikely to be useful
     * otherwise.
     *
     * @return the next task, or {@code null} if none are available
     */
    protected static ForkJoinTask<?> peekNextLocalTask() {
        Thread t; ForkJoinPool.WorkQueue q;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            q = ((ForkJoinWorkerThread)t).workQueue;
        else
            q = ForkJoinPool.commonSubmitterQueue();
        return (q == null) ? null : q.peek();
    }
    /**
     * Unschedules and returns, without executing, the next task
     * queued by the current thread but not yet executed, if the
     * current thread is operating in a ForkJoinPool.  This method is
     * designed primarily to support extensions, and is unlikely to be
     * useful otherwise.
     *
     * @return the next task, or {@code null} if none are available
     */
    protected static ForkJoinTask<?> pollNextLocalTask() {
        Thread t;
        return ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            ((ForkJoinWorkerThread)t).workQueue.nextLocalTask() :
            null;
    }
    /**
     * If the current thread is operating in a ForkJoinPool,
     * unschedules and returns, without executing, the next task
     * queued by the current thread but not yet executed, if one is
     * available, or if not available, a task that was forked by some
     * other thread, if available. Availability may be transient, so a
     * {@code null} result does not necessarily imply quiescence of
     * the pool this task is operating in.  This method is designed
     * primarily to support extensions, and is unlikely to be useful
     * otherwise.
     *
     * @return a task, or {@code null} if none are available
     */
    protected static ForkJoinTask<?> pollTask() {
        Thread t; ForkJoinWorkerThread wt;
        return ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            (wt = (ForkJoinWorkerThread)t).pool.nextTaskFor(wt.workQueue) :
            null;
    }
    // tag operations
    /**
     * Returns the tag for this task.
     *
     * @return the tag for this task
     * @since 1.8
     */
    public final short getForkJoinTaskTag() {
        return (short)status;
    }
    /**
     * Atomically sets the tag value for this task.
     *
     * @param tag the tag value
     * @return the previous value of the tag
     * @since 1.8
     */
    public final short setForkJoinTaskTag(short tag) {
        for (int s;;) {
            if (U.compareAndSwapInt(this, STATUS, s = status,
                                    (s & ~SMASK) | (tag & SMASK)))
                return (short)s;
        }
    }
    /**
     * Atomically conditionally sets the tag value for this task.
     * Among other applications, tags can be used as visit markers
     * in tasks operating on graphs, as in methods that check: {@code
     * if (task.compareAndSetForkJoinTaskTag((short)0, (short)1))}
     * before processing, otherwise exiting because the node has
     * already been visited.
     *
     * @param e the expected tag value
     * @param tag the new tag value
     * @return {@code true} if successful; i.e., the current value was
     * equal to e and is now tag.
     * @since 1.8
     */
    public final boolean compareAndSetForkJoinTaskTag(short e, short tag) {
        for (int s;;) {
            if ((short)(s = status) != e)
                return false;
            if (U.compareAndSwapInt(this, STATUS, s,
                                    (s & ~SMASK) | (tag & SMASK)))
                return true;
        }
    }
    /**
     * Adaptor for Runnables. This implements RunnableFuture
     * to be compliant with AbstractExecutorService constraints
     * when used in ForkJoinPool.
     */
    static final class AdaptedRunnable<T> extends ForkJoinTask<T>
        implements RunnableFuture<T> {
        final Runnable runnable;
        T result;
        AdaptedRunnable(Runnable runnable, T result) {
            if (runnable == null) throw new NullPointerException();
            this.runnable = runnable;
            this.result = result; // OK to set this even before completion
        }
        public final T getRawResult() { return result; }
        public final void setRawResult(T v) { result = v; }
        public final boolean exec() { runnable.run(); return true; }
        public final void run() { invoke(); }
        private static final long serialVersionUID = 5232453952276885070L;
    }
    /**
     * Adaptor for Runnables without results
     */
    static final class AdaptedRunnableAction extends ForkJoinTask<Void>
        implements RunnableFuture<Void> {
        final Runnable runnable;
        AdaptedRunnableAction(Runnable runnable) {
            if (runnable == null) throw new NullPointerException();
            this.runnable = runnable;
        }
        public final Void getRawResult() { return null; }
        public final void setRawResult(Void v) { }
        public final boolean exec() { runnable.run(); return true; }
        public final void run() { invoke(); }
        private static final long serialVersionUID = 5232453952276885070L;
    }
    /**
     * Adaptor for Runnables in which failure forces worker exception
     */
    static final class RunnableExecuteAction extends ForkJoinTask<Void> {
        final Runnable runnable;
        RunnableExecuteAction(Runnable runnable) {
            if (runnable == null) throw new NullPointerException();
            this.runnable = runnable;
        }
        public final Void getRawResult() { return null; }
        public final void setRawResult(Void v) { }
        public final boolean exec() { runnable.run(); return true; }
        void internalPropagateException(Throwable ex) {
            rethrow(ex); // rethrow outside exec() catches.
        }
        private static final long serialVersionUID = 5232453952276885070L;
    }
    /**
     * Adaptor for Callables
     */
    static final class AdaptedCallable<T> extends ForkJoinTask<T>
        implements RunnableFuture<T> {
        final Callable<? extends T> callable;
        T result;
        AdaptedCallable(Callable<? extends T> callable) {
            if (callable == null) throw new NullPointerException();
            this.callable = callable;
        }
        public final T getRawResult() { return result; }
        public final void setRawResult(T v) { result = v; }
        public final boolean exec() {
            try {
                result = callable.call();
                return true;
            } catch (Error err) {
                throw err;
            } catch (RuntimeException rex) {
                throw rex;
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }
        public final void run() { invoke(); }
        private static final long serialVersionUID = 2838392045355241008L;
    }
    /**
     * Returns a new {@code ForkJoinTask} that performs the {@code run}
     * method of the given {@code Runnable} as its action, and returns
     * a null result upon {@link #join}.
     *
     * @param runnable the runnable action
     * @return the task
     */
    public static ForkJoinTask<?> adapt(Runnable runnable) {
        return new AdaptedRunnableAction(runnable);
    }
    /**
     * Returns a new {@code ForkJoinTask} that performs the {@code run}
     * method of the given {@code Runnable} as its action, and returns
     * the given result upon {@link #join}.
     *
     * @param runnable the runnable action
     * @param result the result upon completion
     * @param <T> the type of the result
     * @return the task
     */
    public static <T> ForkJoinTask<T> adapt(Runnable runnable, T result) {
        return new AdaptedRunnable<T>(runnable, result);
    }
    /**
     * Returns a new {@code ForkJoinTask} that performs the {@code call}
     * method of the given {@code Callable} as its action, and returns
     * its result upon {@link #join}, translating any checked exceptions
     * encountered into {@code RuntimeException}.
     *
     * @param callable the callable action
     * @param <T> the type of the callable's result
     * @return the task
     */
    public static <T> ForkJoinTask<T> adapt(Callable<? extends T> callable) {
        return new AdaptedCallable<T>(callable);
    }
    // Serialization support
    private static final long serialVersionUID = -7721805057305804111L;
    /**
     * Saves this task to a stream (that is, serializes it).
     *
     * @param s the stream
     * @throws java.io.IOException if an I/O error occurs
     * @serialData the current run status and the exception thrown
     * during execution, or {@code null} if none
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        s.defaultWriteObject();
        s.writeObject(getException());
    }
    /**
     * Reconstitutes this task from a stream (that is, deserializes it).
     * @param s the stream
     * @throws ClassNotFoundException if the class of a serialized object
     *         could not be found
     * @throws java.io.IOException if an I/O error occurs
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        Object ex = s.readObject();
        if (ex != null)
            setExceptionalCompletion((Throwable)ex);
    }
    // Unsafe mechanics
    private static final sun.misc.Unsafe U;
    private static final long STATUS;
    static {
        exceptionTableLock = new ReentrantLock();
        exceptionTableRefQueue = new ReferenceQueue<Object>();
        exceptionTable = new ExceptionNode[EXCEPTION_MAP_CAPACITY];
        try {
            U = sun.misc.Unsafe.getUnsafe();
            Class<?> k = ForkJoinTask.class;
            STATUS = U.objectFieldOffset
                (k.getDeclaredField("status"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

RecursiveAction.java
public abstract class RecursiveAction extends ForkJoinTask<Void> {
    private static final long serialVersionUID = 5232453952276485070L;
    /**
     * The main computation performed by this task.
     */
    protected abstract void compute();
    /**
     * Always returns {@code null}.
     *
     * @return {@code null} always
     */
    public final Void getRawResult() { return null; }
    /**
     * Requires null completion value.
     */
    protected final void setRawResult(Void mustBeNull) { }
    /**
     * Implements execution conventions for RecursiveActions.
     */
    protected final boolean exec() {
        compute();
        return true;
    }
}

RecursiveTask .java    
public abstract class RecursiveTask<V> extends ForkJoinTask<V> {
    private static final long serialVersionUID = 5232453952276485270L;
    /**
     * The result of the computation.
     */
    V result;
    /**
     * The main computation performed by this task.
     * @return the result of the computation
     */
    protected abstract V compute();
    public final V getRawResult() {
        return result;
    }
    protected final void setRawResult(V value) {
        result = value;
    }
    /**
     * Implements execution conventions for RecursiveTask.
     */
    protected final boolean exec() {
        result = compute();
        return true;
    }
}



原文
地址:http://www.infoq.com/cn/articles/fork-join-introduction

1. 什么是Fork/Join框架

Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
我们再通过Fork和Join这两个单词来理解下Fork/Join框架,Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。比如计算1+2+。。+10000,可以分割成10个子任务,每个子任务分别对1000个数进行求和,最终汇总这10个子任务的结果。Fork/Join的运行流程图如下:

2. 工作窃取算法

工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如下:
那么为什么需要使用工作窃取算法呢?假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
相关厂商内容
相关赞助商
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

3. Fork/Join框架的介绍

我们已经很清楚Fork/Join框架的需求了,那么我们可以思考一下,如果让我们来设计一个Fork/Join框架,该如何设计?这个思考有助于你理解Fork/Join框架的设计。
第一步分割任务。首先我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。
第二步执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。
Fork/Join使用两个类来完成以上两件事情:

4. 使用Fork/Join框架

让我们通过一个简单的需求来使用下Fork/Join框架,需求是:计算1+2+3+4的结果。
使用Fork/Join框架首先要考虑到的是如何分割任务,如果我们希望每个子任务最多执行两个数的相加,那么我们设置分割的阈值是2,由于是4个数字相加,所以Fork/Join框架会把这个任务fork成两个子任务,子任务一负责计算1+2,子任务二负责计算3+4,然后再join两个子任务的结果。
因为是有结果的任务,所以必须继承RecursiveTask,实现代码如下:
通过这个例子让我们再来进一步了解ForkJoinTask,ForkJoinTask与一般的任务的主要区别在于它需要实现compute方法,在这个方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务。如果不足够小,就必须分割成两个子任务,每个子任务在调用fork方法时,又会进入compute方法,看看当前子任务是否需要继续分割成孙任务,如果不需要继续分割,则执行当前子任务并返回结果。使用join方法会等待子任务执行完并得到其结果。

5. Fork/Join框架的异常处理

ForkJoinTask在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过ForkJoinTask的getException方法获取异常。使用如下代码:
if(task.isCompletedAbnormally()){ System.out.println(task.getException());}
getException方法返回Throwable对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没有抛出异常则返回null。

6. Fork/Join框架的实现原理

ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责存放程序提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务。
ForkJoinTask的fork方法实现原理。当我们调用ForkJoinTask的fork方法时,程序会调用ForkJoinWorkerThread的pushTask方法异步的执行这个任务,然后立即返回结果。代码如下:
public final ForkJoinTask fork() { ((ForkJoinWorkerThread) Thread.currentThread()) .pushTask(this); return this; }
pushTask方法把当前任务存放在ForkJoinTask 数组queue里。然后再调用ForkJoinPool的signalWork()方法唤醒或创建一个工作线程来执行任务。代码如下:
final void pushTask(ForkJoinTask t) { ForkJoinTask[] q; int s, m; if ((q = queue) != null) { // ignore if queue removed long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE; UNSAFE.putOrderedObject(q, u, t); queueTop = s + 1; // or use putOrderedInt if ((s -= queueBase) <= 2) pool.signalWork();else if (s == m) growQueue(); } }
ForkJoinTask的join方法实现原理。Join方法的主要作用是阻塞当前线程并等待获取结果。让我们一起看看ForkJoinTask的join方法的实现,代码如下:
public final V join() { if (doJoin() != NORMAL) return reportResult(); else return getRawResult();}private V reportResult() { int s; Throwable ex; if ((s = status) == CANCELLED) throw new CancellationException();if (s == EXCEPTIONAL && (ex = getThrowableException()) != null) UNSAFE.throwException(ex); return getRawResult();}
首先,它调用了doJoin()方法,通过doJoin()方法得到当前任务的状态来判断返回什么结果,任务状态有四种:已完成(NORMAL),被取消(CANCELLED),信号(SIGNAL)和出现异常(EXCEPTIONAL)。
让我们再来分析下doJoin()方法的实现代码:
private int doJoin() { Thread t; ForkJoinWorkerThread w; int s; boolean completed; if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) { if ((s = status) < 0) return s; if ((w = (ForkJoinWorkerThread)t).unpushTask(this)) { try { completed = exec(); } catch (Throwable rex) { return setExceptionalCompletion(rex); } if (completed) return setCompletion(NORMAL); } return w.joinTask(this); } else return externalAwaitDone(); }
在doJoin()方法里,首先通过查看任务的状态,看任务是否已经执行完了,如果执行完了,则直接返回任务状态,如果没有执行完,则从任务数组里取出任务并执行。如果任务顺利执行完成了,则设置任务状态为NORMAL,如果出现异常,则纪录异常,并将任务状态设置为EXCEPTIONAL。

7. 参考资料

8. 作者介绍

方腾飞,花名清英,并发编程网站站长。目前在阿里巴巴微贷事业部工作。并发编程网:http://ifeve.com,个人微博:http://weibo.com/kirals,欢迎通过我的微博进行技术交流。
感谢张龙对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。

学习文章
1.聊聊并发(八)——Fork/Join框架介绍:http://www.infoq.com/cn/articles/fork-join-introduction
2.ForkJoin框架:http://www.cnblogs.com/549294286/p/3907059.html
3.Jdk1.7 JUC源码增量解析(3)-ForkJoin-非ForkJoin任务的执行过程:http://brokendreams.iteye.com/blog/2258068

项目经历 很重要! 注意EBS 数据表修改组件

原文
接着上一篇对红黑树的分析,本文对B树,及其应用进行介绍。
http://blog.csdn.net/v_july_v/article/details/6530142  算法本身的分析
http://tech.meituan.com/mysql-index.html  mysql性能 慢查询 分析
http://blog.codinglabs.org/articles/theory-of-mysql-index.html   mysql索引算法分析
http://yemengying.com/2016/05/24/mysql-tuning/
https://segmentfault.com/a/1190000003072424  mysql索引
 
在一般情况下,红黑树的查找性能确实已经足够好了,但是在数据足够的大的情况下,比如说数据库中的数据,单纯的以红黑树的算法查找还是要花一定的时间,比如说数据库一般存储在磁盘上,而数据量大,树的深度必定高,磁盘IO读写过多(会导致磁盘频繁的移动,而移动就是最消耗时间的)。解决方案就是磁盘的一次查找能够缩小数据的范围,而不仅仅是2叉树的查找仅过滤掉一半的数据。如果 有一种算法可以使得每次查找后,能够过滤掉部分的数据,而不仅仅是一半的数据,这样效率很提高很多。因此类似2叉树的多叉树应运而生。
硬件
关于磁盘的硬件原理,网上有很多资料,在此不在详细介绍,本文只做介绍。记住一点,磁盘上的数据的读写时间由数据的定位(查找柱面号、盘面号、块号 通过移动臂确定柱面,盘片的旋转确定 磁道上的盘号)的查找时间,和位运算传输到内存的时间。前者比后者大的不是一个数据级的,因此,尽量减小数据的查找是提高查找速度的有效办法。
B树
B树 在国内也被人称为B-树。
B 树是为了磁盘或其它存储设备而设计的一种多叉(相对于二叉,B树每个内结点有多个分支,即多叉)平衡查找树。很多数据库系统用的就是 B树或者其变形的结构。
如下一颗高度为3,度树为3,阶树为4的树。 
B树的性质 :
用度或者阶定义数,大同小异。本质是一样的。以阶来定义,阶数为m, 每个结点的最小子树为[m/2] 的上界,向下取整数。最大为m-1。也就是说 内结点的最大子树为m。节点元素个数  [ceil(m / 2)-1]<= n <= m-1
而树的高度  
关于B-Tree有一系列有趣的性质,例如一个度为d的B-Tree,设其索引N个key,则其树高h的上限为logd((N+1)/2),检索一个key,其查找节点个数的渐进复杂度为O(logdN)。从这点可以看出,B-Tree是一个非常有效率的索引数据结构。
如上图所示,是一个B树,节点中还要包含元素的值,而在总结点一定的情况下,每个结点所占的空间是相对稳定的,由为涉及到盘块大多的话,一个节点需要涉及到好几次IO,效率 不高,因此,在节点空间一定情况下,尽量存储多的元素个数。
B+ 树 
B+-tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
    举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+ 树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
索引 
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。而mysql不同的引擎使用不同的索引结构。 而本文研究的是B+树实现的索引。
使用B+树的原因
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。
而内存数据的读写不存在机械操作,仅与次数据相关,与数据的距离无关。

MySQL索引实现  

这部分引用自    http://blog.codinglabs.org/articles/theory-of-mysql-index.html  中关于mysql索引的介绍

myISAM索引实现

MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。下图是MyISAM索引的原理图:
在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。
MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分。

InnoDB索引实现

虽然InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM截然不同。
第一个重大区别是InnoDB的数据文件本身就是索引文件。从上文知道,MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。
 
第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域。使用辅助索引需要查找 两次索引。聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
MySQL中的索引可以以一定顺序引用多个列,这种索引叫做联合索引,一般的,一个联合索引是一个有序元组<a1, a2, …, an>,其中各个元素均为数据表的一列。

最左前缀原理

只能按指定的顺序,依次匹配,如果中间某一条件,无法精确匹配,则后面条件无用。

索引选择性与前缀索引

SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles;
前缀索引 可以减小联合索引的长度。前缀索引兼顾索引大小和查询速度,但是其缺点是不能用于ORDER BY和GROUP BY操作,也不能用于Covering index(即当索引本身包含查询所需全部数据时,不再访问数据文件本身)。

InnoDB的主键选择与插入优化

在使用InnoDB存储引擎时,如果没有特别的需要,请永远使用一个与业务无关的自增字段作为主键。
经常看到有帖子或博客讨论主键选择问题,有人建议使用业务无关的自增主键,有人觉得没有必要,完全可以使用如学号或身份证号这种唯一字段作为主键。不论支持哪种论点,大多数论据都是业务层面的。如果从数据库索引优化角度看,使用InnoDB引擎而不使用自增主键绝对是一个糟糕的主意。
上文讨论过InnoDB的索引实现,InnoDB使用聚集索引,数据记录本身被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。
如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。如下图所示:
 
这样就会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。
如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置:
 
此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。
因此,只要可以,请尽量在InnoDB上采用自增字段做主键。

原文地址:https://my.oschina.net/ovirtKg/blog/759759


关于InnoDB的事务实现机制,其实本人现在也还没总结好,暂且为了应对面试,总结如下几点:
事务的四个特性
原子性:要么成功,要么失败,不会存在执行一半的情况。
一致性:事务只能够让数据库从一种一致的状态到另外一种一致的状态(其实我认为这只是说物理数据的一致性,只是说保证数据不会发生物理性的损坏,说并发安全也可以,但是和隔离性有冲突)
隔离性:多个事务并发执行下的安全问题,事务之间需要有合适的隔离机制。
持久性:事务一旦提交成功,哪怕数据库奔溃,断电,事务的改动都应该持久化下来(实例重启时恢复)
关于InnoDB的四个事务隔离等级
读未提交:最低隔离级别,无法避免不可重复读、脏读、 幻读和丢失更新。
读已提交:可以避免脏读,无法避免、不可重复读、脏读、 和丢失更新。
可重复读:可以避免脏读、不可重复读,幻读(InnoDB特殊规则保证),无法避免丢失更新
串行化:可以避免一切并发问题。
InnoDB的事务实现
事务的实现无非就是围绕着事务的原子性、一致性、隔离线和持久性进行设计!可以有不同的设计策略,但是大体上是相同的,几个核心点如下:
1.如何保证原子性、一致性和持久性
首先明确几个概念
 具体操作如下:
        当事务执行时,所有写操作都会提前生成redo log和undo log并且持久化到磁盘,前者是为了支持重做,后者是为了支持撤销。
        随后写操作会写到缓冲池中的数据页或者索引页,或者插入缓冲当中,定期会被进行合并。
        当缓冲池中的数据要写入磁盘时,先写入磁盘共享区,再写入数据文件(从而可以保证物理文件即使被破坏,也能够从磁盘共享区恢复)
        当数据库实例崩溃重启时,就可以根据上面的机制进行恢复,精妙。
        可以参见:
         InnoDB三大特性之两次写
         InnoDB三大特性之插入缓冲

2.如何保证隔离性
      之前一直认为数据库隔离级别是通过共享锁和独占锁的封锁策略实现的,这个虽然能够完成,但是实际上性能损失很大,所以现在的数据库引擎的实现都很复杂。
      简单的说InnoDB引擎的隔离性是通过MVCC(多版本并发控制)和一定的封锁策略实现的。
      可以认为InnoDB的隔离性是通过以下策略实现的(不完善):
      可参考:
       InnoDB原理之事务隔离级别的实现原理
       MySQL中的表锁、行锁、页锁、排它锁、共享锁、读锁和写锁
       依靠封锁策略实现事务隔离级别的原理
       InnoDB原理之Next-Key Lock浅谈


微服务架构解析(附思维导图)

思维导图


介绍

微服务架构(Microservice Architecture)是一种架构概念
旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦
将功能分解到离散的各个服务当中,从而降低系统的耦合性,并提供更加灵活的服务支持。

传统开发模式和微服务的区别

优点

  • 开发简单,集中式管理
  • 基本不会重复开发
  • 功能都在本地,没有分布式的管理和调用消耗

缺点

  1. 效率低:开发都在同一个项目改代码,相互等待,冲突不断
  2. 维护难:代码功功能耦合在一起,新人不知道何从下手
  3. 不灵活:构建时间长,任何小修改都要重构整个项目,耗时
  4. 稳定性差:一个微小的问题,都可能导致整个应用挂掉
  5. 扩展性不够:无法满足高并发下的业务需求

微服务架构特征

官方的定义:

  1. 一系列的独立的服务共同组成系统
  2. 单独部署,跑在自己的进程中
  3. 每个服务为独立的业务开发
  4. 分布式管理
  5. 非常强调隔离性

大概的标准

  1. 分布式服务组成的系统
  2. 按照业务,而不是技术来划分组织
  3. 做有生命的产品而不是项目
  4. 强服务个体和弱通信( Smart endpoints and dumb pipes )
  5. 自动化运维( DevOps )
  6. 高度容错性
  7. 快速演化和迭代

SOA和微服务的区别

  • SOA喜欢重用,微服务喜欢重写
  • SOA喜欢水平服务,微服务喜欢垂直服务
  • SOA喜欢自上而下,微服务喜欢自下而上

实践微服务

客户端如何访问这些服务

一般在后台N个服务和UI之间一般会一个代理或者叫API Gateway
作用:
  • 提供统一服务入口,让微服务对前台透明
  • 聚合后台的服务,节省流量,提升性能
  • 提供安全,过滤,流控等API管理功能

每个服务之间如何通信

  • REST(JAX-RS,Spring Boot)
  • RPC(Thrift, Dubbo)

服务发现服务注册

  • zookeeper
  • dubbo

服务挂了,如何解决

  • 重试机制
  • 限流
  • 熔断机制
  • 负载均衡
  • 降级(本地缓存)
参考:Netflix的Hystrix

优缺点

  • 优点
    复杂度可控,独立按需扩展,技术选型灵活,容错,可用性高
  • 缺点
    多服务运维难度,系统部署依赖,服务间通信成本,数据一致性,系统集成测试,重复工作,性能监控等

思考

微服务对我们的思考,更多的是思维上的转变。对于微服务架构:技术上不是问题,意识比工具重要。
关于微服务的几点设计出发点:
  1. 应用程序的核心是业务逻辑,按照业务或客户需求组织资源(这是最难的)
  2. 做有生命的产品,而不是项目
  3. 头狼战队,全栈化
  4. 后台服务贯彻Single Responsibility Principle(单一职责原则)
  5. VM->Docker (to PE)
  6. DevOps (to PE)
同时,对于开发同学,有这么多的中间件和强大的PE支持固然是好事,我们也需要深入去了解这些中间件背后的原理,知其然知其所以然,在有限的技术资源如何通过开源技术实施微服务?
最后,一般提到微服务都离不开DevOps和Docker,理解微服务架构是核心,devops和docker是工具,是手段。

参考

http://www.cnblogs.com/imyalost/p/6792724.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
http://www.jianshu.com/p/77ce2dbd1d6e?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
http://kb.cnblogs.com/page/520922/
http://www.infoq.com/cn/articles/seven-uservices-antipatterns
http://www.csdn.net/article/2015-08-07/2825412
http://blog.csdn.net/mindfloating/article/details/45740573
http://blog.csdn.net/sunhuiliang85/article/details/52976210
http://www.oschina.net/news/70121/microservice
(本文完)


微服务架构介绍

微服务架构(Microservice Architecture)是一种架构概念,旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦,将功能分解到离散的各个服务当中,从而降低系统的耦合性,并提供更加灵活的服务支持。

传统开发模式和微服务的区别

优点

  • 开发简单,集中式管理
  • 基本不会重复开发
  • 功能都在本地,没有分布式的管理和调用消耗

缺点

  1. 效率低:开发都在同一个项目改代码,相互等待,冲突不断
  2. 维护难:代码功功能耦合在一起,新人不知道何从下手
  3. 不灵活:构建时间长,任何小修改都要重构整个项目,耗时
  4. 稳定性差:一个微小的问题,都可能导致整个应用挂掉
  5. 扩展性不够:无法满足高并发下的业务需求

微服务架构特征

官方的定义

  1. 一系列的独立的服务共同组成系统
  2. 单独部署,跑在自己的进程中
  3. 每个服务为独立的业务开发
  4. 分布式管理
  5. 非常强调隔离性

大概的标准

  1. 分布式服务组成的系统
  2. 按照业务,而不是技术来划分组织
  3. 做有生命的产品而不是项目
  4. 强服务个体和弱通信( Smart endpoints and dumb pipes )
  5. 自动化运维( DevOps )
  6. 高度容错性
  7. 快速演化和迭代

SOA和微服务的区别

  • SOA喜欢重用,微服务喜欢重写
  • SOA喜欢水平服务,微服务喜欢垂直服务
  • SOA喜欢自上而下,微服务喜欢自下而上

微服务架构当中的多个要素

1.服务注册、服务发现和变更下发(Eureka、ZooKeeper等)

2.服务与服务之间的调用方式(同步、异步、REST、RPC、Dubbo、消息队列......)

3.客户端对服务的访问方式(包括直接访问和API Gateway,API Gateway可以实现权限控制、负载均衡和错误转移)

4.数据的去中心化管理(完全隔离的业务数据库+中心数据库的架构)

5.权限的中心化管理(Redis+spring-security oauth、SSO)

6.使用分布式事务、分布式锁或TICKET幂等保证并发安全

7.中间件(消息队列、RPC、读写分离中间件、分库分表中间件、缓存中间件、权限认证中间件、分布式session......)

8.高效缓存(一致性哈希)

9.负载均衡、故障转移和服务熔断机制(Ribbion&Feign、Hytrix)

10.服务监控和服务管理(dubbo和spring cloud两套体系都提供了自己的服务监控和管理平台)

11.日志监控和日志管理(Logstash + kibana+ElasticSearch)

12.devops(自动化运维)

13.持续集成部署(K8S & Docker)

.......

注意:java界当中,微服务架构的两大典型框架是dubbo和spring cloud。可以通过学习这两个框架,从而掌握微服务-分布式架构中的多个点

在这儿挑几个重要的讲讲

1.服务注册、服务发现和变更下发
  服务注册、服务发现和服务的变更下发是服务治理的基本功能。
  要想实现这种功能,必须进行服务配置信息的存储、协同等工作。
  好在已经有两款著名的分布式协作框架可以帮助我们构建,那就是ZooKeeper和Eureka。
  更一般的情况,我们直接使用Dubbo或者spring cloud,通过简单配置,我们就能够直接享受这种功能。

2.服务与服务之间的调用方式
   服务于服务之间的调用方式按照等待方式可以分为两种:
  同步:REST API和大部分的RPC框架,前者以spring cloud为典型,后者以dubbo为典型。
  异步:消息队列(RabbitMQ)或者CRON(Elastic-Job),spring cloud对这两者都有很好的框架性支持,dubbo只能手动集成第三方。

  严格的说,按照Marting Flower对微服务的描述,服务与服务之间、服务和客户端之间同步调用都应该采用REST API通讯。
  
3.客户端对服务访问方式
  第一种方式是客户端直接调用服务,现在已经不常用了,如下:
  第二种方式是在后台N个服务和UI之间一般会有一个代理或者叫API Gateway,如下图所示:
  API Gateway的作用:
  • 提供统一服务入口,让微服务对前台透明
  • 聚合后台的服务,节省流量,提升性能
  • 提供中心化的安全认证,请求过滤,流控等API管理功能
  • 在Gateway处可以很方便的实现负载均衡和故障转移。
  spring cloud直接集成了Netflix的Zuul来实现API网关,dubbo一般是需要通过框架集成RPC,再对外转换为REST接口,然后才能使用相关网关技术。

4.数据的去中心化管理
单体架构中,不同功能的服务模块都把数据存储在某个中心数据库中,如下:
微服务方式,多个服务之间的设计相互独立,数据也应该相互独立(比如,某个微服务的数据库结构定义方式改变,可能会中断其它服务),因此,每个微服务都应该有自己的私有业务数据库,也可能会有公共的中心数据库。
数据去中心化的核心要点:
数据的去中心化,进一步降低了微服务之间的耦合度,不同服务可以采用不同的数据库技术(SQL、NoSQL等)。

5.权限的中心化管理
用户认证和资源授权是应用系统中不得不谈论的话题,对于微服务系统,因为系统多而细,因此必须要使用中心化的权限认证管理。
一般而言有如下两种方案:
(1)基于spring security oauth2的token认证,使用Redis作为token存储介质。
(2)SSO登录。

6.使用分布式事务、分布式锁或TICKET幂等保证并发和数据安全
 分布式事务相关链接:
  Spring的分布式事务实现
  聊聊分布式事务,再说说解决方案
  分布式事务 TCC-Transaction 源码分析 —— TCC 实现
 分布式锁相关链接:
 分布式锁之三种分布式锁实现
 分布式锁之zookeeper的分布式锁实现(多种)
 TICKET幂等:
 使用Ticket实现HTTP API的幂等性

7.中间件
   消息队列、RPC、读写分离中间件、分库分表中间件、作业中间件、缓存中间件、权限认证中间件、分布式session......)
   消息队列:RabbitMQ,ApacheMQ,Kafka .....
   RPC:dubbo(多种协议),Thrift,RMI,soap(webservices)等等。
   读写分离中间件:sharding-jdbc
   缓存中间件:Ehcache、Memory-cache、Redis
   作业中间件:Elastic-job
   权限认证中间件:JWT、spring security 、spring security oauth
    分布式Session:Session同步、粘性Session、基于缓存服务器的session共享,如下图所示:
8.高效缓存
  一致性哈希具有单调性,可以避免服务器变更对缓存的影响,还可以通过使用虚拟机节点调节每台物理缓存机器所承受的权重。
  参考: 一致性哈希算法


9.负载均衡、故障转移和服务熔断机制
  负载均衡: Spring Cloud 使用 Ribbon 和 Feign做负载均衡
  故障转移:待总结
  服务熔断: Spring Cloud熔断器 Hystrix


10.服务监控和日志管理
 dubbo和spring cloud两套体系都提供了自己的服务监控和管理平台
 参见: 监控与管理dubbo服务


11.日志监控和日志管理
    日志监控和日志管理一方面是可以通过日志监视和统计微服务系统中各个组件的健康状态,统计bug数量等等
    另外一个方面是基于全文搜索,可以给开发人员提供一个中心化的日志查看和搜索平台,从而提高开发和维护效率。
    经典Logstash + kibana+ElasticSearch:
    参考: 基于ElasticSearch+Logstash+Kibana的日志监控平台
  

微服务架构的优缺点

微服务架构的思考

微服务对我们的思考,更多的是思维上的转变。对于微服务架构:技术上不是问题,意识比工具重要。
关于微服务的几点设计出发点:
  1. 应用程序的核心是业务逻辑,按照业务或客户需求组织资源(这是最难的)
  2. 做有生命的产品,而不是项目
  3. 头狼战队,全栈化
  4. 后台服务贯彻Single Responsibility Principle(单一职责原则)
  5. VM->Docker (to PE)
  6. DevOps (to PE)
最后,一般提到微服务都离不开DevOps和Docker,理解微服务架构是核心,devops和docker是工具,是手段



#1. B树
  B树及B-树,是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。
  B树,概括来说是一个一般化的二叉查找树(binary search tree),可以拥有多于2个子节点。与自平衡二叉查找树不同,B树为系统大块数据的读写操作做了优化。B树减少定位记录时所经历的中间过程,从而加快存取速度。B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实作上。
##1.1 B树的性质
M为树的阶数,B-树或为空树,否则满足下列条件:
1. 定义任意非叶子结点最多只有M个儿子;且M>2;
2. 根结点的儿子数为[2, M];
3. 除根结点以外的非叶子结点的儿子数为[M/2, M];
4. 每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)
5. 非叶子结点的关键字个数=指向儿子的指针个数-1;
6. 非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
7. 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
8. 所有叶子结点位于同一层;
如:(M=3) 
  B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。
##1.2 B树的运用场景
另外,B树通过保证内部节点至少半满来最小化空间浪费。一棵B树可以处理任意数目的插入和删除。
#2. B+树
  B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一颗B+树包含根节点、内部节点和叶子节点。根节点可能是一个叶子节点,也可能是一个包含两个或两个以上孩子节点的节点。
  B+ 树通常用于数据库和操作系统的文件系统中。NTFS, ReiserFS, NSS, XFS, JFS, ReFS 和BFS等文件系统都在使用B+树作为元数据索引。B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+ 树元素自底向上插入。
##2.1 B+树的性质
B+树是B-树的变体,也是一种多路搜索树,其定义基本与B-树同,除了:
1. 非叶子结点的子树指针与关键字个数相同;
2. 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);
3. 为所有叶子结点增加一个链指针;
4. 所有关键字都在叶子结点出现;
 
##2.2 B+树与B树的区别
1. 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
2. 不可能在非叶子结点命中;
3. 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
4. 更适合文件索引系统
   B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
#3. B*树
B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针;
 
   B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2);
B+树的分裂:   
当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
B*树的分裂:
当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
所以,B*树分配新结点的概率比B+树要低,空间使用率更高
#4 总结
  在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点 中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;
  在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率从1/2提高到2/3;


#1. 概述
  红黑树(Red Black Tree) 是一种自平衡二叉查找树,红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
  它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。
#2. 性质
 红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
1、节点是红色或黑色。
2、根是黑色。
3、所有叶子都是黑色(叶子是NIL节点)。
4、每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
5、从任一节点到其每个叶子的所有简单路径(不包含重复的点的路径)都包含相同数目的黑色节点。
下面是一个具体的红黑树的图例:
 
#3. 操作
  因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。然而,在红黑树上进行插入操作和删除操作会导致不再匹配红黑树的性质。恢复红黑树的性质需要少量(O(log n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操作时间仍可以保持为O(log n)次。
##3.1插入操作
  我们首先以二叉查找树的方法增加节点并标记它为红色。(如果设为黑色,就会导致根到叶子的路径上有一条路上,多一个额外的黑节点,这个是很难调整的。但是设为红色节点后,可能会导致出现两个连续红色节点的冲突,那么可以通过颜色调换(color flips)和树旋转来调整。)下面要进行什么操作取决于其他临近节点的颜色。同人类的家族树中一样,我们将使用术语叔父节点来指一个节点的父节点的兄弟节点。
注意:
性质1和性质3总是保持着。 性质4只在增加红色节点、重绘黑色节点为红色,或做旋转时受到威胁。 性质5只在增加黑色节点、重绘红色节点为黑色,或做旋转时受到威胁。 
 
  在下面将要插入的节点标为N,N的父节点标为P,N的祖父节点标为G,N的叔父节点标为U。在图中展示的任何颜色要么是由它所处情形这些所作的假定,要么是假定所暗含(imply)的。
情形1:
  新节点N位于树的根上,没有父节点。在这种情形下,我们把它重绘为黑色以满足性质2。因为它在每个路径上对黑节点数目增加一,性质5匹配。
情形2:
  新节点的父节点P是黑色,所以性质4没有失效(新节点是红色的)。在这种情形下,树仍是有效的。性质5也未受到威胁,尽管新节点N有两个黑色叶子子节点;但由于新节点N是红色,通过它的每个子节点的路径就都有同通过它所替换的黑色的叶子的路径同样数目的黑色节点,所以依然满足这个性质。
情形3:
  如果父节点P和叔父节点U二者都是红色,(此时新插入节点N做为P的左子节点或右子节点都属于情形3,这里右图仅显示N做为P左子的情形)则我们可以将它们两个重绘为黑色并重绘祖父节点G为红色(用来保持性质5)。现在我们的新节点N有了一个黑色的父节点P。因为通过父节点P或叔父节点U的任何路径都必定通过祖父节点G,在这些路径上的黑节点数目没有改变。但是,红色的祖父节点G可能是根节点,这就违反了性质2,也有可能祖父节点G的父节点是红色的,这就违反了性质4。
  为了解决这个问题,我们在祖父节点G上递归地进行情形1的整个过程。(把G当成是新加入的节点进行各种情形的检查)
 
情形4:
  父节点P是红色而叔父节点U是黑色或缺少,并且新节点N是其父节点P的右子节点而父节点P又是其父节点的左子节点。在这种情形下,我们进行一次左旋转调换新节点和其父节点的角色;接着,我们按情形5处理以前的父节点P以解决仍然失效的性质4。注意这个改变会导致某些路径通过它们以前不通过的新节点N(比如图中1号叶子节点)或不通过节点P(比如图中3号叶子节点),但由于这两个节点都是红色的,所以性质5仍有效。
情形5:
  父节点P是红色而叔父节点U是黑色或缺少,新节点N是其父节点的左子节点,而父节点P又是其父节点G的左子节点。在这种情形下,我们进行针对祖父节点G的一次右旋转;在旋转产生的树中,以前的父节点P现在是新节点N和以前的祖父节点G的父节点。我们知道以前的祖父节点G是黑色,否则父节点P就不可能是红色(如果P和G都是红色就违反了性质4,所以G必须是黑色)。我们切换以前的父节点P和祖父节点G的颜色,结果的树满足性质4。性质5也仍然保持满足,因为通过这三个节点中任何一个的所有路径以前都通过祖父节点G,现在它们都通过以前的父节点P。在各自的情形下,这都是三个节点中唯一的黑色节点。
 
##3.2删除操作
  对于二叉查找树,在删除带有两个非叶子儿子的节点的时候,我们找到要么在它的左子树中的最大元素、要么在它的右子树中的最小元素,并把它的值转移到要删除的节点中。
  我们首先把要删除的节点替换为它的儿子。出于方便,称呼这个儿子为N(在新的位置上),称呼它的兄弟(它父亲的另一个儿子)为S。在下面的示意图中,我们还是使用P称呼N的父亲,SL称呼S的左儿子,SR称呼S的右儿子。 
情形1:
  N是新的根。在这种情形下,我们就做完了。我们从所有路径去除了一个黑色节点,而新根是黑色的,所以性质都保持着。
情形2:
  S是红色。在这种情形下我们在N的父亲上做左旋转,把红色兄弟转换成N的祖父,我们接着对调N的父亲和祖父的颜色。完成这两个操作后,尽管所有路径上黑色节点的数目没有改变,但现在N有了一个黑色的兄弟和一个红色的父亲(它的新兄弟是黑色因为它是红色S的一个儿子),所以我们可以接下去按情形4、情形5或情形6来处理。
  
情形3:
  N的父亲、S和S的儿子都是黑色的。在这种情形下,我们简单的重绘S为红色。结果是通过S的所有路径,它们就是以前不通过N的那些路径,都少了一个黑色节点。因为删除N的初始的父亲使通过N的所有路径少了一个黑色节点,这使事情都平衡了起来。但是,通过P的所有路径现在比不通过P的路径少了一个黑色节点,所以仍然违反性质5。要修正这个问题,我们要从情形1开始,在P上做重新平衡处理。
 
情形4:
  S和S的儿子都是黑色,但是N的父亲是红色。在这种情形下,我们简单的交换N的兄弟和父亲的颜色。这不影响不通过N的路径的黑色节点的数目,但是它在通过N的路径上对黑色节点数目增加了一,添补了在这些路径上删除的黑色节点。
 
情形5:
  S是黑色,S的左儿子是红色,S的右儿子是黑色,而N是它父亲的左儿子。在这种情形下我们在S上做右旋转,这样S的左儿子成为S的父亲和N的新兄弟。我们接着交换S和它的新父亲的颜色。所有路径仍有同样数目的黑色节点,但是现在N有了一个黑色兄弟,他的右儿子是红色的,所以我们进入了情形6。N和它的父亲都不受这个变换的影响。
 
情形6:
   S是黑色,S的右儿子是红色,而N是它父亲的左儿子。在这种情形下我们在N的父亲上做左旋转,这样S成为N的父亲(P)和S的右儿子的父亲。我们接着交换N的父亲和S的颜色,并使S的右儿子为黑色。子树在它的根上的仍是同样的颜色,所以性质3没有被违反。但是,N现在增加了一个黑色祖先:要么N的父亲变成黑色,要么它是黑色而S被增加为一个黑色祖父。所以,通过N的路径都增加了一个黑色节点。
此时,如果一个路径不通过N,则有两种可能性:
在任何情况下,在这些路径上的黑色节点数目都没有改变。所以我们恢复了性质4。在示意图中的白色节点可以是红色或黑色,但是在变换前后都必须指定相同的颜色。
 

计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是{\displaystyle O(\log {n})}。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G. M. Adelson-VelskyE. M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。
节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。
同一个树在高度平衡之后的样子
目录  [隐藏

操作[编辑]

AVL树的基本操作一般涉及运作同在不平衡的二叉查找树所运作的同样的算法。但是要进行预先或随后做一次或多次所谓的"AVL旋转"。
以下图表以四列表示四种情况,每行表示在该种情况下要进行的操作。在左左和右右的情况下,只需要进行一次旋转操作;在左右和右左的情况下,需要进行两次旋转操作。

删除[编辑]

从AVL树中删除,可以通过把要删除的节点向下旋转成一个叶子节点,接着直接移除这个叶子节点来完成。因为在旋转成叶子节点期间最多有log n个节点被旋转,而每次AVL旋转耗费固定的时间,所以删除处理在整体上耗费O(log n) 时间。

搜寻[编辑]

可以像普通二叉查找树一样的进行,所以耗费O(log n)时间,因为AVL树总是保持平衡的。不需要特殊的准备,树的结构不会由于查找而改变。(这是与伸展树搜寻相对立的,它会因为搜寻而变更树结构。)

实现描述[编辑]

假设平衡因子是左子树的高度减去右子树的高度所得到的值,又假设由于在二叉排序树上插入节点而失去平衡的最小子树根节点的指针为a(即a是离插入点最近,且平衡因子绝对值超过1的祖先节点),则失去平衡后进行的规律可归纳为下列四种情况:
  1. 单向右旋平衡处理LL:由于在*a的左子树根节点的左子树上插入节点,*a的平衡因子由1增至2,致使以*a为根的子树失去平衡,则需进行一次右旋转操作;
  2. 单向左旋平衡处理RR:由于在*a的右子树根节点的右子树上插入节点,*a的平衡因子由-1变为-2,致使以*a为根的子树失去平衡,则需进行一次左旋转操作;
  3. 双向旋转(先左后右)平衡处理LR:由于在*a的左子树根节点的右子树上插入节点,*a的平衡因子由1增至2,致使以*a为根的子树失去平衡,则需进行两次旋转(先左旋后右旋)操作。
  4. 双向旋转(先右后左)平衡处理RL:由于在*a的右子树根节点的左子树上插入节点,*a的平衡因子由-1变为-2,致使以*a为根的子树失去平衡,则需进行两次旋转(先右旋后左旋)操作。
在平衡的二叉排序树BBST (Balancing Binary Search Tree)上插入一个新的数据元素e的递归算法可描述如下:
  1. 若BBST为空树,则插入一个数据元素为e的新节点作为BBST的根节点,树的深度增1;
  2. 若e的关键字和BBST的根节点的关键字相等,则不进行;
  3. 若e的关键字小于BBST的根节点的关键字,而且在BBST的左子树中不存在和e有相同关键字的节点,则将e插入在BBST的左子树上,并且当插入之后的左子树深度增加(+1)时,分别就下列不同情况处理之:
    1. BBST的根节点的平衡因子为-1(右子树的深度大于左子树的深度,则将根节点的平衡因子更改为0,BBST的深度不变;
    2. BBST的根节点的平衡因子为0(左、右子树的深度相等):则将根节点的平衡因子更改为1,BBST的深度增1;
    3. BBST的根节点的平衡因子为1(左子树的深度大于右子树的深度):则若BBST的左子树根节点的平衡因子为1:则需进行单向右旋平衡处理,并且在右旋处理之后,将根节点和其右子树根节点的平衡因子更改为0,树的深度不变;
  4. 若e的关键字大于BBST的根节点的关键字,而且在BBST的右子树中不存在和e有相同关键字的节点,则将e插入在BBST的右子树上,并且当插入之后的右子树深度增加(+1)时,分别就不同情况处理之。
AVL树的调平(Erlang的实现)
1 balance(null) -> null; 2 balance({null, _, null}=Tree) -> Tree; 3 balance({Left, Value, Right}=Tree) -> 4 Diff = count(Left)-count(Right), 5 if (Diff < 2) and (Diff > -2) -> {balance(Left), Value, balance(Right)}; 6 (Diff > 1) -> balance(rotate_right(Tree)); 7 (Diff< -1) -> balance(rotate_left(Tree)); 8 true -> exit('This is impossible!') 9 end.10 11 rotate_right({Left, Value, Right}) ->12 merge_max(Left, {null, Value, Right}).13 14 rotate_left({Left, Value, Right}) ->15 merge_min(Right, {Left, Value, null}).16 17 merge_min({null, Value, Right}, Tree2) ->18 {Tree2, Value, Right};19 merge_min({Left, _, _}, Tree2) ->20 merge_min(Left, Tree2).21 22 merge_max({Left , Value, null}, Tree2) ->23 {Left, Value, Tree2};24 merge_max({_, _, Right}, Tree2) ->25 merge_max(Right, Tree2).

AVL节点数计算[编辑]

高度为h的AVL树,节点数N最多{\displaystyle 2^{h}-1}; 最少{\displaystyle {\frac {\Phi ^{h+2}}{\sqrt {5}}}-1} ( 其中{\displaystyle \Phi ={\frac {1+{\sqrt {5}}}{2}}=1.618} )。
最少节点数n如以费伯纳西数列可以用数学归纳法证明:
{\displaystyle N_{h}} = {\displaystyle F_{h+2}} - 1 ({\displaystyle F_{h+2}}是Fibonacci polynomial)。
即:
{\displaystyle N_{0}} = 0 (表示AVL Tree高度为0的节点总数)
{\displaystyle N_{1}} = 1 (表示AVL Tree高度为1的节点总数)
{\displaystyle N_{2}} = 2 (表示AVL Tree高度为2的节点总数)
{\displaystyle N_{h}} = {\displaystyle N_{h-1}} + {\displaystyle N_{h-2}} + 1 (表示AVL Tree高度为h的节点总数)
换句话说,当节点数为N时,高度h最多为{\displaystyle log_{\Phi }({\sqrt {5}}*(N+1))-2}

参见[编辑]

引用[编辑]

外部链接[编辑]



MyISAM和Innodb引擎的索引是怎么实现的
MySQL有多种存储引擎,所使用的存储引擎都是多种多样,但MyISAM和Innodb的索引数据结构都是基于B+树!
但MyISAM的索引和Innodb的索引还是有些许不同。
MyISAM的索引实现
MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。下图是MyISAM索引的原理图:
在MyISAM中,所有的索引都是这样存储的,只有组合索引、单字段索引、唯一性索引几种索引概念,没有聚集索引!
Innodb的索引实现
Innodb引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录或者数据记录的主键。存放数据记录的索引是聚集索引,只能有一个,存放主键的索引可以有多个,叫做非聚集索引,下图是Innodb的聚集索引的原理图:
如果Innodb的主键是非聚集索引,因此如果主键不作为聚集索引(主键只是普通的唯一性索引)那实际上会经历两次查询,严重降低查询性能。这也是为什么很多情况下,MyISAM的读取更高效的原因之一。

为什么MyISAM和Innodb的索引使用B+树
1.索引文件很大,不可能全部存储在内存中,故要存储到磁盘上
2.索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数(为什么使用B-/+Tree,还跟磁盘存取原理有关。)
3.B+树所有的Data域在叶子节点,一般来说都会进行一个优化,就是将所有的叶子节点用指针串起来,这样遍历叶子节点就能获得全部数据。

聚集索引注意事项
聚集索引一个表只能够有一个,MySQL的 聚集索引默认建立在主键上,可以显示指定在其它字段或者其它多个字段上。
聚集索引和除了data节点,和普通索引是一样的,聚集索引因为是物理顺序,所以对于范围查询特别有效果!
聚集索引妥善使用,当使用多个字段作为聚集索引时,一定要把最频繁查询的字段放在前列,聚集索引如果按照字段的先后顺序查,那能够有效果,否则没有效果!

索引的最左前缀原理
索引是可以比较大小的,当使用where name like '张%'是能够使用到索引的,但是使用where name like ‘%小明’没有效果的。

为什么Innodb最好在自增主键上建立聚集索引
这个还是看具体的需求,如果真的需要特别多的查询,但插入很少的话(只读数据表),还是建议使用频繁的业务字段作为聚集索引
之所以在自增主键上使用聚集索引,是因为插入数据时,可以直接插到叶子节点的最右端,避免寻找插入位置和移动数据。


原文地址:http://thephper.com/?p=142

板子之前做过2年web开发培训(入门?),获得挺多学生好评,这是蛮有成就感的一件事,准备花点时间根据当时的一些备课内容整理出一系列文章出来,希望能给更多人带来帮助,这是系列文章的第一篇
注:科普文章一篇,大牛绕道

索引是做什么的?

索引用于快速找出在某个列中有一特定值的行。不使用索引,MySQL必须从第1条记录开始然后读完整个表直到找出相关的行。
表越大,花费的时间越多。如果表中查询的列有一个索引,MySQL能快速到达一个位置去搜寻到数据文件的中间,没有必要看所有数据。
大多数MySQL索引(PRIMARY KEY、UNIQUE、INDEX和FULLTEXT)在B树中存储。只是空间列类型的索引使用R-树,并且MEMORY表还支持hash索引。

索引好复杂,我该怎么理解索引,有没一个更形象点的例子?

有,想象一下,你面前有本词典,数据就是书的正文内容,你就是那个cpu,而索引,则是书的目录

索引越多越好?

大多数情况下索引能大幅度提高查询效率,但:

索引的字段类型问题

 like 不能用索引?

想象一下,你在看一本成语词典,目录是按成语拼音顺序建立,查询需求是,你想找以 “一”字开头的成语(”一%“),和你想找包含一字的成语(“%一%”)
<,<=,=,>,>=,BETWEEN,IN
<>,not in ,!=则不行

什么样的字段不适合建索引?

 一次查询能用多个索引吗?

不能

多列查询该如何建索引?

一次查询只能用到一个索引,所以 首先枪毙 a,b各建索引方案
a还是b? 谁的区分度更高(同值的最少),建谁!
当然,联合索引也是个不错的方案,ab,还是ba,则同上,区分度高者,在前

联合索引的问题?

where a = “xxx” 可以使用 AB 联合索引
where b = “xxx” 则不可 (再想象一下,这是书的目录?)
所以,大多数情况下,有AB索引了,就可以不用在去建一个A索引了

哪些常见情况不能用索引?

也即
select * from test where mobile = 13711112222;
可是无法用到mobile字段的索引的哦(如果mobile是char 或 varchar类型的话)
btw,千万不要尝试用int来存手机号(为什么?自己想!要不自己试试)
 

覆盖索引(Covering Indexes)拥有更高效率

索引包含了所需的全部值的话,就只select 他们,换言之,只select 需要用到的字段,如无必要,可尽量避免select *

NULL 的问题

NULL会导致索引形同虚设,所以在设计表结构时应避免NULL 的存在(用其他方式表达你想表达的NULL,比如 -1?)

如何查看索引信息,如何分析是否正确用到索引?

show index from tablename;
explain select ……;
关于explain,改天可以找个时间专门写一篇入门帖,在此之前,可以尝试 google

了解自己的系统,不要过早优化!

过早优化,一直是个非常讨厌而又时刻存在的问题,大多数时候就是因为不了解自己的系统,不知道自己系统真正的承载能力
比如:几千条数据的新闻表,每天几百几千次的正文搜索,大多数时候我们可以放心的去like,而不要又去建一套全文搜索什么的,毕竟cpu还是比人脑厉害太多

分享个小案例:

曾经有个朋友找板子,说:大师帮看看,公司网站打不开
板子笑了笑:大师可不敢当啊,待我看看再说
板子花了10分钟分析了下:中小型企业站,量不大(两三万pv每天),独立服务器,数据量不大(100M不到),应该不至于太慢
某个外包团队做的项目,年久失修,彻底改造?不现实!
于是,板子花了20分钟给可以加索引的字段都加上了索引,于是,世界安静了
朋友说:另外一个哥们说,优化至少得2w外包费,你只用30分钟,看来,大师你是当之无愧了,选个最好的餐馆吧
板子:那就来点西餐吧,常熟路地铁站肯德基等你!

最后:永远别忘记的关键词 sql注入




锁是开发过程中十分常见的工具,在处理高并发请求的时候和订单数据的时候往往需要锁来帮助我们保证数据的安全。
分布式锁的一些场景
场景1.前端点击太快,导致后端重复调用接口。两次调用一个接口,这样就会产生同一个请求执行了两次,而从用户的角度出发,他是因为太卡而点了两次,他的目标是执行一次请求。
场景2.对于高并发场景,我们往往需要引入分布式缓存,来加快整个系统的响应速度。但是缓存是有失效机制的,如果某一时刻缓存失效,而此时有大量的请求过来,那么所有的请求会瞬间直接打到DB上,那么这么大的并发量,DB可能是扛不住的。那么这里需要引入一个保护机制。当发生“缓存击穿”的时候加锁,从而保护DB不被拖垮。
场景3:数据存在则更新,不存在则插入的并发问题
如何实现分布式锁
看完了上面的场景,其实分布式锁的场景一直在我们身边。说分布式锁之前,应该先说一下java提供的锁,比较能单机解决的并发问题,没必要引入分布式的解决方案。
java提供了两种内置的锁的实现,一种是由JVM实现的synchronized和JDK提供的Lock,当你的应用是单机或者说单进程应用时,可以使用synchronized或Lock来实现锁。
但是,当你的应用涉及到多机、多进程共同完成时,例如现在的互联网架构,一般都是分布式的RPC框架来支撑,那么这样你的Server有多个,由于负载均衡的路由规则随机,相同的请求可能会打到不同的Server上进行处理,那么这时候就需要一个全局锁来实现多个线程(不同的进程)之间的同步。
实现全局的锁需要依赖一个第三方系统,此系统需要满足高可用、一致性比较强同时能应付高并发的请求。
常见的处理办法有三种:数据库、缓存、分布式协调系统。数据库和缓存是比较常用的,但是分布式协调系统是不常用的。

数据库实现分布式锁####

利用DB来实现分布式锁,有两种方案。两种方案各有好坏,但是总体效果都不是很好。但是实现还是比较简单的。
1.利用主键唯一规则
我们知道数据库是有唯一主键规则的,主键不能重复,对于重复的主键会抛出主键冲突异常。
了解JDK reentrantlock的人都知道,reentrantlock是利用了OS的CAS特性实现的锁。主要是维护一个全局的状态,每次竞争锁都会CAS修改锁的状态,修改成功之后就占用了锁,失败的加入到同步队列中,等待唤醒。
其实这和分布式锁实现方案基本是一致的,首先我们利用主键唯一规则,在争抢锁的时候向DB中写一条记录,这条记录主要包含锁的id、当前占用锁的线程名、重入的次数和创建时间等,如果插入成功表示当前线程获取到了锁,如果插入失败那么证明锁被其他人占用,等待一会儿继续争抢,直到争抢到或者超时为止。
这里我主要写了一个简单的实现:
package com.fhr.concurrentdemo.distributedlock;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.sql.DataSource;
/**
 * 利用MySQL行锁实现分布式锁
 *
 * @author HuaRanFan
 *
 */
public class MysqlPrimaryLock {
     private static DataSource dataSource;
     static {
          try {
              Class.forName("com.mysql.jdbc.Driver");
          } catch (ClassNotFoundException e) {
              e.printStackTrace();
          }
          // String url = "jdbc:mysql://10.0.0.212:3308/dbwww_lock?user=lock_admin&password=lock123";
          // datasource初始化逻辑
     }
     private Connection getConnection() throws SQLException {
          return dataSource.getConnection();
     }
     /**
      * 加锁
      *
      * @param lockID
      */
     public void lock(String lockID) {
          acquire(lockID);
     }
     /**
      * 获取锁
      *
      * @param lockID
      * @return
      */
     public boolean acquire(String lockID) {
          String sql = "insert into test_lock('id','count','thName','addtime') VALUES (?,?,?,?)";
          
          while (true) {
              try {
                   PreparedStatement statement = getConnection().prepareStatement(sql);
                   statement.setString(1, lockID);
                   statement.setInt(2, 1);
                   statement.setLong(1, System.currentTimeMillis());
                   boolean ifsucess = statement.execute();// 如果成功,那么就是获取到了锁
                   
                   if (ifsucess)
                        return true;
              } catch (SQLException e) {
                   e.printStackTrace();
              }
              try {
                   Thread.sleep(1000);
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
              continue;
          }
     }
     /**
      * 超时获取锁
      *
      * @param lockID
      * @param timeOuts
      * @return
      * @throws InterruptedException
      */
     public boolean acquire(String lockID, long timeOuts) throws InterruptedException {
          String sql = "insert into test_lock('id','count','thName','addtime') VALUES (?,?,?,?)";
          
          long futureTime = System.currentTimeMillis() + timeOuts;
          long ranmain = timeOuts;
          long timerange = 500;
          while (true) {
              CountDownLatch latch = new CountDownLatch(1);
              try {
                   PreparedStatement statement = getConnection().prepareStatement(sql);
                   statement.setString(1, lockID);
                   statement.setInt(2, 1);
                   statement.setLong(1, System.currentTimeMillis());
                   boolean ifsucess = statement.execute();// 如果成功,那么就是获取到了锁
                   if (ifsucess)
                        return true;
              } catch (SQLException e) {
                   e.printStackTrace();
              }
              latch.await(timerange, TimeUnit.MILLISECONDS);
              ranmain = futureTime - System.currentTimeMillis();
              if (ranmain <= 0)
                   break;
              if (ranmain < timerange) {
                   timerange = ranmain;
              }
              continue;
          }
          return false;
     }
     /**
      * 释放锁
      *
      * @param lockID
      * @return
      * @throws SQLException
      */
     public boolean unlock(String lockID) throws SQLException {
          String sql = "DELETE from test_lock where id = ?";
          PreparedStatement statement = getConnection().prepareStatement(sql);
          statement.setString(1, lockID);
          boolean ifsucess = statement.execute();
          if (ifsucess)
              return true;
          return false;
     }
}
这里是利用主键冲突规则,加入了id','count','thName','addtime',count主要是为了重入计数,thName为了判断占用锁的线程,addtime是记录占用时间。上面代码没有实现重入的逻辑。
重入主要实现思路是,在每次获取锁之前去取当前锁的信息,如果锁的线程是当前线程,那么更新锁的count+1,并且执行锁之后的逻辑。如果不是当前锁,那么进行重试。释放的时候也要进行count-1,最后减到0时,删除锁标识释放锁。
优点:实现简单
缺点:没有超时保护机制,mysql存在单点,并发量大的时候请求量太大、没有线程唤醒机制,用异常去控制逻辑多少优点恶心。
对于超时保护:如果可能,可以采用定时任务去扫描超过一定阈值的锁,并删除。但是也会存在,锁住的任务执行时间很长,删除锁会导致并发问题。所以需要对超时时间有一个很好的预估。
对于单点问题:有条件可以搞一个主从,但是为了一个锁来搞一个主从是不是优点浪费?同时主从切换的时候系统不可用,这也是一个问题。
并发量大的时候请求量太大:因为这种实现方式是没有锁的唤醒机制的,不像reentrantlock在同步队列中的节点,可以通过唤醒来避免多次的循环请求。但是分布式环境数据库这种锁的实现是不能做到唤醒的。所以只能将获取锁的时间间隔调高,避免死循环给系统和DB带来的巨大压力。这样也牺牲了系统的吞吐量,因为总会有一定的间隔锁是空闲的。
用异常去控制逻辑多少优点恶心:就不说了,每次失败都抛异常.....
2.利用Mysql行锁的特性
Mysql是有表锁、页锁和行锁的机制的,可以利用这个机制来实现锁。这里尽量使用行锁,它的吞吐量是最高的。
package com.fhr.concurrentdemo.distributedlock;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.sql.DataSource;
/**
 * 使用MySQL行锁来实现分布式锁 依靠了事务,所以如果想没有侵入性,需要使用ThreadLocal来构建全局变量。
 *
 * @author HuaRanFan
 *
 */
public class MySQLRowLock {
     private static DataSource dataSource;
     static {
          try {
              Class.forName("com.mysql.jdbc.Driver");
          } catch (ClassNotFoundException e) {
              e.printStackTrace();
          }
          // String url =
          // "jdbc:mysql://10.0.0.212:3308/dbwww_lock?user=lock_admin&password=lock123";
          // datasource初始化逻辑
     }
     private static ThreadLocal<Connection> threadLocalConnection = new ThreadLocal<Connection>() {
          @Override
          protected Connection initialValue() {
              try {
                   return dataSource.getConnection();
              } catch (SQLException e) {
                   e.printStackTrace();
                   return null;
              }
          }
     };
     private Connection getConnection() throws SQLException {
          return threadLocalConnection.get();
     }
     /**
      * 超时获取锁
      *
      * @param lockID
      * @param timeOuts
      * @return
      * @throws InterruptedException
      */
     public boolean acquireByUpdate(String lockID, long timeOuts) throws InterruptedException, SQLException {
          String sql = "SELECT id from test_lock where id = ? for UPDATE ";
          long futureTime = System.currentTimeMillis() + timeOuts;
          long ranmain = timeOuts;
          long timerange = 500;
          getConnection().setAutoCommit(false);
          while (true) {
              CountDownLatch latch = new CountDownLatch(1);
              try {
                   PreparedStatement statement = getConnection().prepareStatement(sql);
                   statement.setString(1, lockID);
                   statement.setInt(2, 1);
                   statement.setLong(1, System.currentTimeMillis());
                   boolean ifsucess = statement.execute();// 如果成功,那么就是获取到了锁
                   if (ifsucess)
                        return true;
              } catch (SQLException e) {
                   e.printStackTrace();
              }
              latch.await(timerange, TimeUnit.MILLISECONDS);
              ranmain = futureTime - System.currentTimeMillis();
              if (ranmain <= 0)
                   break;
              if (ranmain < timerange) {
                   timerange = ranmain;
              }
              continue;
          }
          return false;
     }
     /**
      * 释放锁
      *
      * @param lockID
      * @return
      * @throws SQLException
      */
     public void unlockforUpdtate(String lockID) throws SQLException {
          getConnection().commit();
     }
}
利用for update加显式的行锁,这样就能利用这个行级的排他锁来实现分布式锁了,同时unlock的时候只要释放commit这个事务,就能达到释放锁的目的。
优点:实现简单
缺点:连接池爆满和事务超时的问题单点的问题,单点问题,行锁升级为表锁的问题,并发量大的时候请求量太大、没有线程唤醒机制。
连接池爆满和事务超时的问题单点的问题:利用事务进行加锁的时候,query需要占用数据库连接,在行锁的时候连接不释放,这就会导致连接池爆满。同时由于事务是有超时时间的,过了超时时间自动回滚,会导致锁的释放,这个超时时间要把控好。
对于单点问题:同上。
并发量大的时候请求量太大:同上。
行锁升级为表锁的问题:Mysql行锁默认需要走索引,如果不走索引会导致锁表,如果可以,在sql中可以强制指定索引。

缓存分布式锁####

缓存实现分布式锁还是比较常见的,因为缓存比较轻量,并且缓存的响应快、吞吐高。最重要的是还有自动失效的机制来保证锁一定能释放。
缓存的分布式锁主要通过Redis实现,当然其他的缓存也是可以的。关于缓存有两种实现吧:
基于SetNX实现
setNX是Redis提供的一个原子操作,如果指定key存在,那么setNX失败,如果不存在会进行Set操作并返回成功。我们可以利用这个来实现一个分布式的锁,主要思路就是,set成功表示获取锁,set失败表示获取失败,失败后需要重试。
具体看下代码:
package com.fhr.concurrentdemo.distributedlock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import redis.clients.jedis.Jedis;
/**
 * 基于Redis缓存实现的锁
 * 核心是setNx方法,setNX是Redis提供的一个原子操作,
 * 如果指定key存在,那么setNX失败,如果不存在会进行Set操作并返回成功。
 * 我们可以利用这个来实现一个分布式的锁,主要思路就是:
 * set成功表示获取锁,set失败表示获取失败,失败后需要重试。
 *
 * @author HuaRanFan
 *
 */
public class RedisLock {
     private Jedis jedisCli = new Jedis("localhost", 6381);
     private int expireTime = 1;
     /**
      * 获取锁
      *
      * @param lockID
      * @return
      */
     public boolean lock(String lockID) {
          while (true) {
              long returnFlag = jedisCli.setnx(lockID, "1");
              if (returnFlag == 1) {
                   System.out.println(Thread.currentThread().getName() + " get lock....");
                   return true;
              }
               System.out.println(Thread.currentThread().getName() + " is trying lock....");
              try {
                   Thread.sleep(2000);
              } catch (InterruptedException e) {
                   e.printStackTrace();
                   return false;
              }
          }
     }
     /**
      * 超时获取锁
      *
      * @param lockID
      * @param timeOuts
      * @return
      */
     public boolean lock(String lockID, long timeOuts) {
          long current = System.currentTimeMillis();
          long future = current + timeOuts;
          long timeStep = 500;
          CountDownLatch latch = new CountDownLatch(1);
          
          while (future > current) {
              long returnFlag = jedisCli.setnx(lockID, "1");
              if (returnFlag == 1) {
                   System.out.println(Thread.currentThread().getName() + " get lock....");
                   jedisCli.expire(lockID, expireTime);
                   return true;
              }
               System.out.println(Thread.currentThread().getName() + " is trying lock....");
              try {
                   latch.await(timeStep, TimeUnit.MILLISECONDS);
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
              current = current + timeStep;
          }
          
          return false;
     }
     public void unlock(String lockId) {
          long flag = jedisCli.del(lockId);
          
          if (flag > 0) {
               System.out.println(Thread.currentThread().getName() + " release lock....");
          } else {
               System.out.println(Thread.currentThread().getName() + " release lock fail....");
          }
     }
}
可以看到,几个线程很好的进行了同步。
这种方式也是有优点和缺点:
优点:实现简单,吞吐量十分客观,对于高并发情况应付自如,自带超时保护,对于网络抖动的情况也可以利用超时删除策略保证不会阻塞所有流程。
缺点:单点问题、没有线程唤醒机制、网络抖动可能会引起锁删除失败。
对单点问题:因为redis一般都是单实例使用,那么对于单点问题,可以做一个主从。当然主从切换的时候也是不可用的,因为主从同步是异步的,可能会并发问题。如果对于主从还是不能保证可靠性的话,可以上Redis集群,对于Redis集群,因为使用了类一致性Hash算法,虽然不能避免节点下线的并发问题(当前的任务没有执行完,其他任务就开始执行),但是能保证Redis是可用的。可用性的问题是出了问题之后的备选方案,如果我们系统天天都出问题还玩毛啊,对于突发情况牺牲一两个请求还是没问题的。
对于线程唤醒机制:分布式锁大多都是这样轮训获取锁的,所以控制住你的重试频率,也不会导致负载特别高的。可能就是吞吐量低点而已。
对于锁删除失败:分布式锁基本都有这个问题,可以对key设置失效时间。这个超时时间需要把控好,过大那么系统吞吐量低,很容易导致超时。如果过小那么会有并发问题,部分耗时时间比较长的任务就要遭殃了。
注意:setNx方法是原子方法,但如果随后手动设置过期时间,那就不是原子的,因此也存在并发问题,但Redis的set操作很多,有相应的原子操作合并了setNX和expire的两条操作的。

基于Zookeeper的分布式锁####

Zookeeper是一个分布式一致性协调框架,主要可以实现选主、配置管理和分布式锁等常用功能,因为Zookeeper的写入都是顺序的,在一个节点创建之后,其他请求再次创建便会失败,同时可以对这个节点进行Watch,如果节点删除会通知其他节点抢占锁。
第一种实现(会产生惊群效应)
可以利用Zookeeper不能重复创建一个节点的特性来实现一个分布式锁,这看起来和redis实现分布式锁很像。但是也是有差异的,后面会详细分析。
主要流程图如下:
上面的流程很简单:
  1. 查看目标Node是否已经创建,已经创建,那么等待锁。
  2. 如果未创建,创建一个瞬时Node,表示已经占有锁。
  3. 如果创建失败,那么证明锁已经被其他线程占有了,那么同样等待锁。
  4. 当释放锁,或者当前Session超时的时候,节点被删除,唤醒之前等待锁的线程去争抢锁。
上面是一个完整的流程,简单的代码实现如下:
import java.io.IOException;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;

/**
* 基于ZooKeeper实现的分布式锁 无顺序保证,有惊群效应
*
* @author HuaRanFan
* @since 2018年3月10日
*/
public class NoFairZooKeeperLock {

    private String zkQurom = "localhost:2181";

    private String lockNameSpace = "/mylock";

    private String nodeString = lockNameSpace + "/test1";

    private ZooKeeper zk;

    public NoFairZooKeeperLock() {
        try {
            zk = new ZooKeeper(zkQurom, 6000, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    System.out.println("Receive event " + watchedEvent);
                    if (Event.KeeperState.SyncConnected == watchedEvent.getState())
                        System.out.println("connection is established...");
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private void ensureRootPath() throws InterruptedException {
        try {
            if (zk.exists(lockNameSpace, true) == null) {
                zk.create(lockNameSpace, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    private void watchNode(String nodeString, final Thread thread) throws InterruptedException {
        try {
            zk.exists(nodeString, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    System.out.println("==" + watchedEvent.toString());
                    if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
                        System.out.println("Threre is a Thread released Lock==============");
                        thread.interrupt();
                    }
                    try {
                        zk.exists(nodeString, new Watcher() {
                            @Override
                            public void process(WatchedEvent watchedEvent) {
                                System.out.println("==" + watchedEvent.toString());
                                if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
                                    System.out.println("Threre is a Thread released Lock==============");
                                    thread.interrupt();
                                }
                                try {
                                    zk.exists(nodeString, true);
                                } catch (KeeperException e) {
                                    e.printStackTrace();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }

                        });
                    } catch (KeeperException e) {
                        e.printStackTrace();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            });
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取锁
     *
     * @return
     * @throws InterruptedException
     */
    public boolean lock() throws InterruptedException {
        @SuppressWarnings("unused")
        String path = null;
        ensureRootPath();
        watchNode(nodeString, Thread.currentThread());
        while (true) {
            try {
                path = zk.create(nodeString, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
            } catch (KeeperException e) {
                System.out.println(Thread.currentThread().getName() + "  getting Lock but can not get");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ex) {
                    System.out.println("thread is notify");
                }
            }
            System.out.println(Thread.currentThread().getName() + "  get Lock...");
            return true;
        }
    }

    /**
     * 释放锁
     */
    public void unlock() {
        try {
            zk.delete(nodeString, -1);
            System.out.println("Thread.currentThread().getName() +  release Lock...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

}



其实上面的实现有优点也有缺点:
优点:
实现比较简单,有通知机制,能提供较快的响应,有点类似reentrantlock的思想,对于节点删除失败的场景由Session超时保证节点能够删除掉。
缺点:
重量级,同时在大量锁的情况下会有“惊群”的问题。
“惊群”就是在一个节点删除的时候,大量对这个节点的删除动作有订阅Watcher的线程会进行回调,这对Zk集群是十分不利的。所以需要避免这种现象的发生。
解决“惊群” (第二种实现  
为了解决“惊群“问题,我们需要放弃订阅一个节点的策略,那么怎么做呢?
  1. 我们将锁抽象成目录,多个线程在此目录下创建瞬时的顺序节点,因为Zk会为我们保证节点的顺序性,所以可以利用节点的顺序进行锁的判断。
  2. 首先创建顺序节点,然后获取当前目录下最小的节点,判断最小节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。
  3. 获取锁失败的节点获取当前节点上一个顺序节点,对此节点注册监听,当节点删除的时候通知当前节点。
  4. 当unlock的时候删除节点之后会通知下一个节点。
上面的实现和reentrantlock的公平锁实现还是比较类似的,下面是简单的实现:
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
/**
 * 基于ZooKeeper实现的分布式锁 有顺序保证,
 * 无惊群效应(依靠顺序队列)
 * @author HuaRanFan
 * @since  2018年3月10日
 */
public class FairZooKeeperLock {
     private String zkQurom = "localhost:2181";
    private String lockName = "/mylock";
    private String lockZnode = null;
    private ZooKeeper zk;
    public FairZooKeeperLock(){
        try {
            zk = new ZooKeeper(zkQurom, 6000, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    System.out.println("Receive event "+watchedEvent);
                    if(Event.KeeperState.SyncConnected == watchedEvent.getState())
                        System.out.println("connection is established...");
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private void ensureRootPath(){
        try {
            if (zk.exists(lockName,true)==null){
                zk.create(lockName,"".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 获取锁
     * @return
     * @throws InterruptedException
     */
    public void lock(){
        String path = null;
        ensureRootPath();
            try {
                path = zk.create(lockName+"/mylock_", "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
                lockZnode = path;
                List<String> minPath = zk.getChildren(lockName,false);
                System.out.println(minPath);
                Collections.sort(minPath);
                System.out.println(minPath.get(0)+" and path "+path);
                String watchNode = null;
                for (int i=minPath.size()-1;i>=0;i--){
                    if(minPath.get(i).compareTo(path.substring(path.lastIndexOf("/") + 1))<0){
                        watchNode = minPath.get(i);
                        break;
                    }
                }
                if (watchNode!=null){
                    final String watchNodeTmp = watchNode;
                    final Thread thread = Thread.currentThread();
                    Stat stat = zk.exists(lockName + "/" + watchNodeTmp,new Watcher() {
                        @Override
                        public void process(WatchedEvent watchedEvent) {
                            if(watchedEvent.getType() == Event.EventType.NodeDeleted){
                                thread.interrupt();
                            }
                            try {
                                zk.exists(lockName + "/" + watchNodeTmp,true);
                            } catch (KeeperException e) {
                                e.printStackTrace();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                    if(stat != null){
                        System.out.println("Thread " + Thread.currentThread().getId() + " waiting for " + lockName + "/" + watchNode);
                    }
                }
                try {
                    Thread.sleep(1000000000);
                }catch (InterruptedException ex){
                    System.out.println(Thread.currentThread().getName() + " notify");
                    System.out.println(Thread.currentThread().getName() + "  get Lock...");
                    return;
                }
            } catch (Exception e) {
               e.printStackTrace();
            }
    }
    /**
     * 释放锁
     */
    public void unlock(){
        try {
            System.out.println(Thread.currentThread().getName() +  "release Lock...");
            zk.delete(lockZnode,-1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }
}


其实上面的实现还是很复杂的,因为你需要反复的去关注Watcher,实现一个Demo可以,做一个生产环境可用的Lock并不容易。因为你的代码bug在生产环境上会引起很严重的bug。
其实对于Zookeeper的一些常用功能是有一些成熟的包实现的,像Curator。Curator的确是足够牛逼,不仅封装了Zookeeper的常用API,也包装了很多常用Case的实现。但是它的编程风格其实还是吧比较难以接受的。
可以用Curator轻易的实现一个分布式锁:
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) )
{
try
{
// do some work inside of the critical section here
}
finally
{
lock.release();
}
}
是的就这么简单,一个直接拿过来可用的轮子。
基于Zookeeper的分布式锁就说完了。基于Zookeeper实现分布式锁,其实是不常用的。虽然它实现锁十分优雅,但编程复杂,同时还要单独维护一套Zookeeper集群,频繁的Watch对Zookeeper集群的压力还是蛮大的,如果不是原有的项目以来Zookeeper,同时锁的量级比较小的话,还是不用为妙
三种分布锁对比:####
Mysql实现比较简单,不需要引入第三个应用,但实现多少有些重,性能不是很好。
Redis的话实现比较简单,同时性能很好,引入集群可以提高可用性。同时定期失效的机制可以解决因网络抖动锁删除失败的问题,所以我比较倾向Redis实现。
Zookeeper实现是有些重的,同时我们还需要维护Zookeeper集群,实现起来还是比较复杂的,实现不好的话还会引起“羊群效应”。如果不是原有系统就依赖Zookeeper,同时压力不大的情况下。一般不使用Zookeeper实现分布式锁。




1. 背景

最近在学习 Zookeeper,在刚开始接触 Zookeeper 的时候,完全不知道 Zookeeper 有什么用。且很多资料都是将 Zookeeper 描述成一个“类 Unix/Linux 文件系统”的中间件,导致我很难将类 Unix/Linux 文件系统的 Zookeeper 和分布式应用联系在一起。后来在粗读了《ZooKeeper 分布式过程协同技术详解》和《从Paxos到Zookeeper 分布式一致性原理与实践》两本书,并动手写了一些 CURD demo 后,初步对 Zookeeper 有了一定的了解。不过比较肤浅,为了进一步加深对 Zookeeper 的认识,我利用空闲时间编写了本篇文章对应的 demo -- 基于 Zookeeper 的分布式锁实现。通过编写这个分布式锁 demo,使我对 Zookeeper 的 watcher 机制、Zookeeper 的用途等有了更进一步的认识。不过我所编写的分布式锁还是比较简陋的,实现的也不够优美,仅仅是个练习,仅供参考使用。好了,题外话就说到这里,接下来我们就来聊聊基于 Zookeeper 的分布式锁实现。

2. 独占锁和读写锁的实现

在本章,我将分别说明独占锁和读写锁详细的实现过程,并配以相应的流程图帮助大家了解实现的过程。这里先说说独占锁的实现。

2.1 独占锁的实现

独占锁又称排它锁,从字面意思上很容易理解他们的用途。即如果某个操作 O1 对访问资源 R1 的过程加锁,在操作 O1 结束对资源 R1访问前,其他操作不允许访问资源 R1。以上算是对独占锁的简单定义了,那么这段定义在 Zookeeper 的“类 Unix/Linux 文件系统”的结构中是怎样实现的呢?在锁答案前,我们先看张图:

图1 独占锁的 Zookeeper 节点结构
如上图,对于独占锁,我们可以将资源 R1 看做是 lock 节点,操作 O1 访问资源 R1 看做创建 lock 节点,释放资源 R1 看做删除 lock 节点。这样我们就将独占锁的定义对应于具体的 Zookeeper 节点结构,通过创建 lock 节点获取锁,删除节点释放锁。详细的过程如下:
  1. 多个客户端竞争创建 lock 临时节点
  2. 其中某个客户端成功创建 lock 节点,其他客户端对 lock 节点设置 watcher
  3. 持有锁的客户端删除 lock 节点或该客户端崩溃,由 Zookeeper 删除 lock 节点
  4. 其他客户端获得 lock 节点被删除的通知
  5. 重复上述4个步骤,直至无客户端在等待获取锁了
上面即独占锁具体的实现步骤,理解起来并不复杂,这里不再赘述。

图2 获取独占锁流程图

2.2 读写锁的实现

说完独占锁的实现,这节来说说读写锁的实现。读写锁包含一个读锁和写锁,操作 O1 对资源 R1 加读锁,且获得了锁,其他操作可同时对资源 R1 设置读锁,进行共享读操作。如果操作 O1 对资源 R1 加写锁,且获得了锁,其他操作再对资源 R1 设置不同类型的锁都会被阻塞。总结来说,读锁具有共享性,而写锁具有排他性。那么在 Zookeeper 中,我们可以用怎样的节点结构实现上面的操作呢?

图3 读写锁的 Zookeeper 节点结构
在 Zookeeper 中,由于读写锁和独占锁的节点结构不同,读写锁的客户端不用再去竞争创建 lock 节点。所以在一开始,所有的客户端都会创建自己的锁节点。如果不出意外,所有的锁节点都能被创建成功,此时锁节点结构如图3所示。之后,客户端从 Zookeeper 端获取 /share_lock 下所有的子节点,并判断自己能否获取锁。如果客户端创建的是读锁节点,获取锁的条件(满足其中一个即可)如下:
  1. 自己创建的节点序号排在所有其他子节点前面
  2. 自己创建的节点前面无写锁节点
如果客户端创建的是写锁节点,由于写锁具有排他性。所以获取锁的条件要简单一些,只需确定自己创建的锁节点是否排在其他子节点前面即可。
不同于独占锁,读写锁的实现稍微复杂一下。读写锁有两种实现方式,各有异同,接下来就来说说这两种实现方式。

读写锁的第一种实现

第一种实现是对 /share_lock 节点设置 watcher,当 /share_lock 下的子节点被删除时,未获取锁的客户端收到 /share_lock 子节点变动的通知。在收到通知后,客户端重新判断自己创建的子节点是否可以获取锁,如果失败,再次等待通知。详细流程如下:
  1. 所有客户端创建自己的锁节点
  2. 从 Zookeeper 端获取 /share_lock 下所有的子节点,并对 /share_lock 节点设置 watcher
  3. 判断自己创建的锁节点是否可以获取锁,如果可以,持有锁。否则继续等待
  4. 持有锁的客户端删除自己的锁节点,其他客户端收到 /share_lock 子节点变动的通知
  5. 重复步骤2、3、4,直至无客户端在等待获取锁了
上述步骤对于的流程图如下:

图4 获取读写锁实现1流程图
上面获取读写锁流程并不复杂,但却存在性能问题。以图3所示锁节点结构为例,第一个锁节点 host1-W-0000000001 被移除后,Zookeeper 会将 /share_lock 子节点变动的通知分发给所有的客户端。但实际上,该子节点变动通知除了能影响 host2-R-0000000002 节点对应的客户端外,分发给其他客户端则是在做无用功,因为其他客户端即使获取了通知也无法获取锁。所以这里需要做一些优化,优化措施是让客户端只在自己关心的节点被删除时,再去获取锁。

读写锁的第二种实现

在了解读写锁第一种实现的弊端后,我们针对这一实现进行优化。这里客户端不再对 /share_lock 节点进行监视,而只对自己关心的节点进行监视。还是以图3的锁节点结构进行举例说明,host2-R-0000000002 对应的客户端 C2 只需监视 host1-W-0000000001 节点是否被删除即可。而 host3-W-0000000003 对应的客户端 C3 只需监视 host2-R-0000000002 节点是否被删除即可,只有 host2-R-0000000002 节点被删除,客户端 C3 才能获取锁。而 host1-W-0000000001 节点被删除时,产生的通知对于客户端 C3 来说是无用的,即使客户端 C3 响应了通知也没法获取锁。这里总结一下,不同客户端关心的锁节点是不同的。如果客户端创建的是读锁节点,那么客户端只需找出比读锁节点序号小的最后一个的写锁节点,并设置 watcher 即可。而如果是写锁节点,则更简单,客户端仅需对该节点的上一个节点设置 watcher 即可。详细的流程如下:
  1. 所有客户端创建自己的锁节点
  2. 从 Zookeeper 端获取 /share_lock 下所有的子节点
  3. 判断自己创建的锁节点是否可以获取锁,如果可以,持有锁。否则对自己关心的锁节点设置 watcher
  4. 持有锁的客户端删除自己的锁节点,某个客户端收到该节点被删除的通知,并获取锁
  5. 重复步骤4,直至无客户端在等待获取锁了
上述步骤对于的流程图如下:

图5 获取读写锁实现2流程图

3. 写在最后

本文较为详细的描述了基于 Zookeeper 分布式锁的实现过程,并根据上面描述的两种锁原理实现了较为简单的分布式锁 demo,代码放在了 github 上,需要的朋友自取。因为这只是一个简单的 demo,代码实现的并不优美,仅供参考。最后,如果你觉得文章还不错的话,欢迎点赞。如果有不妥的地方,也请提出来,我会虚心改之。好了,最后祝大家生活愉快,再见。

参考

本文在知识共享许可协议 4.0 下发布,转载请注明出处
作者:code4fun
为了获得更好的分类阅读体验,
请移步至本人的个人博客:http://www.coolblog.xyz
原文地址:https://segmentfault.com/a/1190000010895869


                                               图一 spring-mvc工作流程图

                                                         图二 spring-mvc类交互示意图
spring工作流程描述
      1. 用户向服务器发送请求,请求被Spring前端控制器Servelt DispatcherServlet捕获;
      2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回;
      3. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。(附注:如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(...)方法
       4.  提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
      HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
      数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
      数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
      数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
      5.  Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;
      6.  根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet ;
      7. ViewResolver 结合Model和View,来渲染视图
      8. 将渲染结果返回给客户端。

Spring工作流程描述
    为什么Spring只使用一个Servlet(DispatcherServlet)来处理所有请求?
     详细见 java EE核心模式-Front Controller(前端控制器模式)
    Spring为什么要结合使用HandlerMapping以及HandlerAdapter来处理Handler?
    符合面向对象中的单一职责原则,代码架构清晰,便于维护,最重要的是代码可复用性高。如HandlerAdapter可能会被用于处理多种Handler。
MVC核心代码
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
    ......
    /**
      * Override the parent class implementation in order to intercept PATCH requests.
      */
     @Override
     protected void service(HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
          HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
          if (HttpMethod.PATCH == httpMethod || httpMethod == null) {
              processRequest(request, response);
          }
          else {
              super.service(request, response);
          }
     }
     /**
      * Delegate GET requests to processRequest/doService.
      * <p>Will also be invoked by HttpServlet's default implementation of {@code doHead},
      * with a {@code NoBodyResponse} that just captures the content length.
      * @see #doService
      * @see #doHead
      */
     @Override
     protected final void doGet(HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
          processRequest(request, response);
     }
     /**
      * Delegate POST requests to {@link #processRequest}.
      * @see #doService
      */
     @Override
     protected final void doPost(HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
          processRequest(request, response);
     }
     /**
      * Delegate PUT requests to {@link #processRequest}.
      * @see #doService
      */
     @Override
     protected final void doPut(HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
          processRequest(request, response);
     }
     /**
      * Delegate DELETE requests to {@link #processRequest}.
      * @see #doService
      */
     @Override
     protected final void doDelete(HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
          processRequest(request, response);
     }
     ......
    /**
      * Process this request, publishing an event regardless of the outcome.
      * <p>The actual event handling is performed by the abstract
      * {@link #doService} template method.
      */
     protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
          long startTime = System.currentTimeMillis();
          Throwable failureCause = null;
          LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
          LocaleContext localeContext = buildLocaleContext(request);
          RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
          ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
          WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
          asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
          initContextHolders(request, localeContext, requestAttributes);
          try {
              doService(request, response);
          }
          catch (ServletException ex) {
              failureCause = ex;
              throw ex;
          }
          catch (IOException ex) {
              failureCause = ex;
              throw ex;
          }
          catch (Throwable ex) {
              failureCause = ex;
              throw new NestedServletException("Request processing failed", ex);
          }
          finally {
              resetContextHolders(request, previousLocaleContext, previousAttributes);
              if (requestAttributes != null) {
                   requestAttributes.requestCompleted();
              }
              if (logger.isDebugEnabled()) {
                   if (failureCause != null) {
                        this.logger.debug("Could not complete request", failureCause);
                   }
                   else {
                        if (asyncManager.isConcurrentHandlingStarted()) {
                             logger.debug("Leaving response open for concurrent processing");
                        }
                        else {
                             this.logger.debug("Successfully completed request");
                        }
                   }
              }
              publishRequestHandledEvent(request, response, startTime, failureCause);
          }
     }
   ......
}


  public class DispatcherServlet extends FrameworkServlet {
      ......   
     /**
      * Exposes the DispatcherServlet-specific request attributes and delegates to {@link #doDispatch}
      * for the actual dispatching.
      */
     @Override
     protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
          if (logger.isDebugEnabled()) {
              String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
              logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
                        " processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]");
          }
          // Keep a snapshot of the request attributes in case of an include,
          // to be able to restore the original attributes after the include.
          Map<String, Object> attributesSnapshot = null;
          if (WebUtils.isIncludeRequest(request)) {
              attributesSnapshot = new HashMap<String, Object>();
              Enumeration<?> attrNames = request.getAttributeNames();
              while (attrNames.hasMoreElements()) {
                   String attrName = (String) attrNames.nextElement();
                   if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) {
                        attributesSnapshot.put(attrName, request.getAttribute(attrName));
                   }
              }
          }
          // Make framework objects available to handlers and view objects.
          request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
          request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
          request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
          request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
          FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
          if (inputFlashMap != null) {
               request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
          }
          request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
          request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
          try {
              doDispatch(request, response);
          }
          finally {
              if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
                   // Restore the original attribute snapshot, in case of an include.
                   if (attributesSnapshot != null) {
                        restoreAttributesAfterInclude(request, attributesSnapshot);
                   }
              }
          }
     }
     /**
      * Process the actual dispatching to the handler.
      * <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
      * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
      * to find the first that supports the handler class.
      * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
      * themselves to decide which methods are acceptable.
      * @param request current HTTP request
      * @param response current HTTP response
      * @throws Exception in case of any kind of processing failure
      */
     protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
          HttpServletRequest processedRequest = request;
          HandlerExecutionChain mappedHandler = null;
          boolean multipartRequestParsed = false;
          WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
          try {
              ModelAndView mv = null;
              Exception dispatchException = null;
              try {
                   processedRequest = checkMultipart(request);
                   multipartRequestParsed = (processedRequest != request);
                   // Determine handler for the current request.
                   mappedHandler = getHandler(processedRequest);
                   if (mappedHandler == null || mappedHandler.getHandler() == null) {
                        noHandlerFound(processedRequest, response);
                        return;
                   }
                   // Determine handler adapter for the current request.
                   HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
                   // Process last-modified header, if supported by the handler.
                   String method = request.getMethod();
                   boolean isGet = "GET".equals(method);
                   if (isGet || "HEAD".equals(method)) {
                        long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                        if (logger.isDebugEnabled()) {
                             logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                        }
                        if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                             return;
                        }
                   }
                   if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                   }
                   // Actually invoke the handler.
                   mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                   if (asyncManager.isConcurrentHandlingStarted()) {
                        return;
                   }
                   applyDefaultViewName(processedRequest, mv);
                   mappedHandler.applyPostHandle(processedRequest, response, mv);
              }
              catch (Exception ex) {
                   dispatchException = ex;
              }
              catch (Throwable err) {
                   // As of 4.3, we're processing Errors thrown from handler methods as well,
                   // making them available for @ExceptionHandler methods and other scenarios.
                   dispatchException = new NestedServletException("Handler dispatch failed", err);
              }
              processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
          }
          catch (Exception ex) {
              triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
          }
          catch (Throwable err) {
              triggerAfterCompletion(processedRequest, response, mappedHandler,
                        new NestedServletException("Handler processing failed", err));
          }
          finally {
              if (asyncManager.isConcurrentHandlingStarted()) {
                   // Instead of postHandle and afterCompletion
                   if (mappedHandler != null) {
                        mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                   }
              }
              else {
                   // Clean up any resources used by a multipart request.
                   if (multipartRequestParsed) {
                        cleanupMultipart(processedRequest);
                   }
              }
          }
     }
       ......
}




Front Controller模式要求在WEB应用系统的前端(Front)设置一个入口控制器(Controller),所有的request请求都被发往该控制器统一处理。
Front Controller一般可以用来做一共同处理比如认证,页面导航,Session管理,国际化或本地化处理等。
在Spring当中,前端控制器是DispatcherServlet。

传统模式弊端
在开发WEB应用系统(但不拘于WEB应用)时,存在很多不恰当的设计方法,比如让客户端(Client,一般指浏览器)可以直接访问各个视图(view,JSP等)。这样逻辑被分散到各个视图中,从而产生了各种问题:
1,对已有的功能修改困难,可维护性低。假如session管理,一旦session内容需要发生改变,则需要修改所有view中的相关代码。
2,很难增加新的功能,缺乏可扩展性。例如,需要在已有的系统中加入安全控制功能,控制用户对某些页面的访问,因为没有统一的处理入口,需要在所有的view中都加上认证代码。
使用Front Controller,强制分离view的显示逻辑与业务处理逻辑。

使用前端控制器模式的意义
1.最大化代码重用,可以直接进行各个请求的认证,页面导航,session管理,国际化或本地处理
2.容易增加新功能,将view和业务处理分离开

最近新的项目架构启用spring boot cloud,SO现在先坐下简单的技术梳理,后边的博客会把spring的技术细节,boot的技术细节重新梳理一遍

1、下面是根据条件初始化bean
 

2、读取配置信息操作
加载配置可以用@PropertySource("classpath:com/ecej/test2/test.properties") 记得要用 private Environment environment; 读取配置
  1. package com.ecej.test2;  
  2.   
  3. importorg.apache.commons.io.IOUtils;  
  4.   
  5. import org.springframework.beans.factory.annotation.Autowired;  
  6.   
  7. import org.springframework.beans.factory.annotation.Value;  
  8.   
  9. import org.springframework.context.annotation.Bean;  
  10.   
  11. importorg.springframework.context.annotation.ComponentScan;  
  12.   
  13. import org.springframework.context.annotation.Configuration;  
  14.   
  15. import org.springframework.context.annotation.PropertySource;  
  16.   
  17. import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;  
  18.   
  19. import org.springframework.core.env.Environment;  
  20.   
  21. import org.springframework.core.io.Resource;  
  22.   
  23. @Configuration  
  24.   
  25. @ComponentScan("com.ecej.test2")  
  26.   
  27. @PropertySource("classpath:com/ecej/test2/test.properties")  
  28.   
  29. public class ELConfig {  
  30.   
  31. @Value("I LOVE YOU")  
  32.   
  33. private String normal;  
  34.   
  35. @Value("#{systemProperties['os.name']}")  
  36.   
  37. private String osName;  
  38.   
  39. @Value("#{T(java.lang.Math).random() * 100.0}")  
  40.   
  41. private double randomNumber;  
  42.   
  43. @Value("#{demoService.another}")  
  44.   
  45. private String fromAnother;  
  46.   
  47. @Value("#classpath:com/ecej/test2/test.properties")  
  48.   
  49. private Resource testFile;  
  50.   
  51. @Value("http://www.baidu.com")  
  52.   
  53. private Resource testUrl;  
  54.   
  55. @Value("${book.name}")  
  56.   
  57. private String bookName;  
  58.   
  59. @Autowired  
  60.   
  61. private Environment environment;  
  62.   
  63. @Bean  
  64.   
  65. public static PropertySourcesPlaceholderConfigurer propertyConfigure() {  
  66.   
  67. return new PropertySourcesPlaceholderConfigurer();  
  68.   
  69. }  
  70.   
  71. public void outputResource() {  
  72.   
  73. try {  
  74.   
  75. System.out.println(normal);  
  76.   
  77. System.out.println(osName);  
  78.   
  79. System.out.println(randomNumber);  
  80.   
  81. System.out.println(fromAnother);  
  82.   
  83. System.out.println(IOUtils.toString(testUrl.getInputStream()));  
  84.   
  85. System.out.println(bookName);  
  86.   
  87. System.out.println(environment.getProperty("book.author"));  
  88.   
  89. catch (Exception e) {  
  90.   
  91. // TODO: handle exception  
  92.   
  93. }  
  94.   
  95. }  
  96.   
  97. }  



3、spring event
事件
1 、首先我们要实现ApplicationListener 实现我们自己的监听
2、 定义我们自己的事件 通过集成ApplicationEvent实现
3、 定义config启动
通过applicationContext 发布事件
[java] view plain copy
  1. @Component  
  2.   
  3. public class DemoPublisher {  
  4.   
  5. @Autowired  
  6.   
  7. private ApplicationContext applicationContext;  
  8.   
  9. public void publish(String msg) {  
  10.   
  11. applicationContext.publishEvent(new DemoEvent(this, msg));  
  12.   
  13. }  
  14.   
  15. }  



4、spring Aware
讲解:bean 和spring是无耦合的,但是如果想用到spring容器的功能资源,就要你的bean知道spring的存在,这就是spring aware


5、多线程
spring通过 TaskExecutor来实现多线程并发编程。使用ThreadPoolExecutor可实现基于线程池的TaskExecutor,使用@EnableAsync开启对异步任务的支持,并通过在实际执行bean方法中使用@Async注解来声明一个异步任务

6、计划任务
通过配置注解@EnableScheduline来开启对计划任务的支持,然后再要执行的任务上加注解@Scheduled
spring通过@Scheduled支持多种类型计划任务,包含cron,fixDelay、fixRate(固定时间执行)

7、条件注解@Conditional


8、组合注解与源注解
源注解:就是可以注解到别的注解上的注解,被注解的注解称之为组合注解。组合注解有源注解的功能。
@Configuration  @ComponentScan 组合出 Wiselyconfiguration

9、@Enable*注解工作原理


10、Spring MVC
下面所讲解的都是配置在MyMVCConfig
通过实现WebApplicationInitializer 等同于web.xml配置
基本配置
spring MVC的定制配置需要我们配置集成一个WebMvcConfigurerAdapter,并在此使用@EnableWebMvc注解,来开启对spring MVC配置支持
静态资源
重写addResourceHandlers方法实现

拦截器配置
类似servlet的Filter
可以让普通bean 实现HanlderIntercepetor接口或者继承HandlerInterceptorAdapter类来实现自定义拦截器
在boot中通过重写WebMvcConfigurerAdapter 的 addInterceptors方法来注册自定义拦截器
@ControllerAdvice
@ExceptionHandler定义全局处理 ,通过value属性可以设置拦截过滤条件
在开发中经常会遇到跳转页面的事情,我们还要单独写一个方法很麻烦,现在可以这样

路径参数配置
在spring mvc中路径参数如果带点“.” ,那点后面的值将被忽略。通过重写configurePathMatch(PathMatchConfigurer) 可不忽略 点后参数

文件上传
demo集合


二 、正式开始spring boot


CLI  命令行控制工具

1、@SpringBootApplication 和入口
这个标签是个组合注解,包含了@Configuration @EnableAutoConfiguration @ComponentScan三个标签
@EnableAutoConfiguration 让spring boot根据类路径中的jar包依赖为当前项目进行自动配置
在spring boot中我们可以使用
@Value("${book.author}")直接注入属性,但是还是感觉一个个注入麻烦啊,SO,我们可以直接映射一个类,用@ConfigurationProperties(prefix="author",locations={"classpath:author.properties"})通过prefix指定前缀,通过locations指定位置

2、spring boot 的web开发
需要定义模版信息的话,使用ViewResolver  ,别忘了在config上加注解@EnableWebMvc
[java] view plain copy
  1. @Bean  
  2.   
  3. public InternalResourceViewResolver viewResolver() {  
  4.   
  5. InternalResourceViewResolver resolver = new InternalResourceViewResolver();  
  6.   
  7. resolver.setPrefix("WEB-INF/classes/views/");  
  8.   
  9. resolver.setSuffix(".jsp");  
  10.   
  11. resolver.setViewClass(JstlView.class);  
  12.   
  13. return resolver;  
  14.   
  15. }  


静态资源默认放在src/main.resources/static 下面

3、静态首页的支持

4、接管spring boot 的web配置
如果boot 提供的配置不是我们需要的,可以通过配置类修改,
注解来实现自己完全控制
@Configuration
@ComponentScan("com.ecej.test.mvc")
@EnableWebMvc
如果我们想即用默认配置,又增加自定义配置,可以集成WebMvcConfigurerAdapter,无需使用@EnableWebMvc注解

5、注册servlet Filter Listener(重点来了)
可以注册ServletRegistrationBean  FilterRegistrationBean  ServletListenerRegistrationBean 的bean来实现

6、Tomcat配置
其实就是servlet容器配置,因为BOOT内置的是tomcat,所以也就叫tomcat配置了
配置都在org.springframework.boot.autoconfigure.web.ServerProperties 中(其实大部分都有这么个配置)
SO,我们只需要在application.properties中配置就好了(如果你想拥别的名字,只需要配置下就行咯,在上边有提到过)。通用的配置都以server作为前缀
例子:
配置容器
server.port=8080
server.session-timeout=2
配置tomcat
server.tomcat.uri-encoding=UTF-8
上边都是配置文件配置,如果想玩玩代码也是可以的,下面介绍代码配置
想配置servlet容器可以实现一个EmbeddedServletContainerCustomizer的接口(注意声明的类要为static)
想直接配置tomcat等则可以直接定义TomcatEmbeddedServletContainerFactory等


7、SSL配置(安全套接层)
生成证书:
JDK下有工具keytool ,他是一个证书管理工具,可以用来生成证书

在你命令执行的当前目录下生成了一个.keystore的证书
配置我们的配置文件
server.port=8000
server.ssl.key-store=.keystore
server.ssl.key-store-password=123456
server.ssl.keyStoreType=JKS
server.ssl.keyAlias:tomcat
此时在启动项目就变成了https的

8、设置自己的Favicon
这个就非常简单了,只需要将自己的favicon.ico文件放在META-INF/resources/   resources/  static/  public/下面任意一个目录下就行了。

9、WebSocket
这到底是个什么鬼呢?
官方说法,就是为浏览器和服务端提供双工异步通信的功能。直接使用WebSocket会使开发非常繁琐的,所以我们使用它的子协议STOMP,它是一个更高级的协议,STOMP协议使用一个基于帧的格式来定义消息,与HTTP的request response类似。
spring boot内置了这玩意,可以看websocket包下的类
需要加入
spring-boot-starter-websocket 包
@EnableWebSocketMessageBroker注解并继承AbstractWebSocketMessageBrokerConfigurer
模式:
广播式  会将消息发送给所有连接了当前的浏览器
代码片段
[java] view plain copy
  1. package com.ecej.demo2.websocket;  
  2.   
  3. importorg.springframework.context.annotation.ComponentScan;  
  4.   
  5. import org.springframework.context.annotation.Configuration;  
  6.   
  7. import org.springframework.messaging.simp.config.MessageBrokerRegistry;  
  8.   
  9. import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;  
  10.   
  11. import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;  
  12.   
  13. import org.springframework.web.socket.config.annotation.StompEndpointRegistry;  
  14.   
  15. @Configuration  
  16.   
  17. @ComponentScan(value = "com.ecej.demo2.websocket")  
  18.   
  19. @EnableWebSocketMessageBroker  
  20.   
  21. public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {  
  22.   
  23. @Override  
  24.   
  25. public void registerStompEndpoints(StompEndpointRegistry register) {  
  26.   
  27. register.addEndpoint("/endpointWisely").withSockJS();  
  28.   
  29. }  
  30.   
  31. @Override  
  32.   
  33. public void configureMessageBroker(MessageBrokerRegistry register) {  
  34.   
  35. register.enableSimpleBroker("/topic");  
  36.   
  37. }  
  38.   
  39. }  


解析:
1、通过@EnableWebSocketMessageBroker 注解开启使用STOMP协议传输基于代理(message broker)的消息,
这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
2、注册STOMP协议的节点,并映射的指定的URL
3、注册一个STOMP的endpoint,并指定使用SocketJS协议
4、配置消息代理(message broker)
5、广播式应配置一个/topic消息代理

10、spring 的事物机制
spring事物机制提供了一个PlatformTransactionManager接口,不同的数据访问技术的事物使用不同的接口实现
声明式事物
使用@Transactional注解在方法上表明该方法需要事物支持,基于AOP实现操作。
使用@EnableTransactionManagement来开启上边的注解
类级别的注解@Transactional会重载方法级别的


11、缓存支持
不同的缓存技术,需要不同的cacheManager


12、异步消息
spring 对JMS和AMQP的支持分别来自于spring-jms 和spring-rabbit
他们分布需要ConnectionFactory来实现连接消息代理,并分别提供了JmsTemplate、RabbitTemplate
spring为JMS 、AMQP提供了@JmsListener @RabbitListener 注解在方法上监听消息代理发布的消息。我们只需要分别通过@EnableJms @EnableRabbit开启支持


Spring容器高层视图
Spring 启动时读取应用程序提供的Bean配置信息,并在Spring容器中生成一份相应的Bean配置注册表,然后根据这张注册表实例化Bean,装配好Bean之间的依赖关系,为上层应用提供准备就绪的运行环境。
Bean缓存池:Map实现
IOC容器介绍
Spring 通过一个配置文件描述 Bean 及 Bean 之间的依赖关系,利用 Java 语言的反射功能实例化 Bean 并建立 Bean 之间的依赖关系。 Spring 的 IoC 容器在完成这些底层工作的基础上,还提供了 Bean 实例缓存、生命周期管理、 Bean 实例代理、事件发布、资源装载等高级服务。
BeanFactory 是 Spring 框架的基础设施,面向 Spring 本身;
ApplicationContext 面向使用 Spring 框架的开发者,几乎所有的应用场合我们都直接使用 ApplicationContext 而非底层的 BeanFactory。
BeanFactory
BeanFactory体系架构:
BeanDefinitionRegistry: Spring 配置文件中每一个节点元素在 Spring 容器里都通过一个 BeanDefinition 对象表示,它描述了 Bean 的配置信息。而 BeanDefinitionRegistry 接口提供了向容器手工注册 BeanDefinition 对象的方法。
BeanFactory 接口位于类结构树的顶端 ,它最主要的方法就是 getBean(String beanName),该方法从容器中返回特定名称的 Bean,BeanFactory 的功能通过其他的接口得到不断扩展:
ListableBeanFactory:该接口定义了访问容器中 Bean 基本信息的若干方法,如查看Bean 的个数、获取某一类型 Bean 的配置名、查看容器中是否包括某一 Bean 等方法;
HierarchicalBeanFactory:父子级联 IoC 容器的接口,子容器可以通过接口方法访问父容器; 通过 HierarchicalBeanFactory 接口, Spring 的 IoC 容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的 Bean,但父容器不能访问子容器的 Bean。Spring 使用父子容器实现了很多功能,比如在 Spring MVC 中,展现层 Bean 位于一个子容器中,而业务层和持久层的 Bean 位于父容器中。这样,展现层 Bean 就可以引用业务层和持久层的 Bean,而业务层和持久层的 Bean 则看不到展现层的 Bean。
ConfigurableBeanFactory:是一个重要的接口,增强了 IoC 容器的可定制性,它定义了设置类装载器、属性编辑器、容器初始化后置处理器等方法;
AutowireCapableBeanFactory:定义了将容器中的 Bean 按某种规则(如按名字匹配、按类型匹配等)进行自动装配的方法;
SingletonBeanRegistry:定义了允许在运行期间向容器注册单实例 Bean 的方法;
例子:
使用 Spring 配置文件为 Car 提供配置信息:beans.xml:
通过 BeanFactory 装载配置文件,启动 Spring IoC 容器:
XmlBeanFactory 通过 Resource 装载 Spring 配置信息并启动 IoC 容器,然后就可以通过 BeanFactory#getBean(beanName)方法从 IoC 容器中获取 Bean 了。通过 BeanFactory 启动IoC 容器时,并不会初始化配置文件中定义的 Bean,初始化动作发生在第一个调用时。
对于单实例( singleton)的 Bean 来说,BeanFactory会缓存 Bean 实例,所以第二次使用 getBean() 获取 Bean 时将直接从 IoC 容器的缓存中获取 Bean 实例。Spring 在 DefaultSingletonBeanRegistry 类中提供了一个用于缓存单实例 Bean 的缓存器,它是一个用ConcurrentHashMap(存储懒汉式Bean)和HashMap(存储饿汉式Bean)实现的缓存器,单实例的 Bean 以 beanName 为键保存在这个Map 中。
值得一提的是,在初始化 BeanFactory 时,必须为其提供一种日志框架,比如使用Log4J, 即在类路径下提供 Log4J 配置文件,这样启动 Spring 容器才不会报错。
ApplicationContext
ApplicationContext 由 BeanFactory 派生而来,提供了更多面向实际应用的功能。
在BeanFactory 中,很多功能需要以编程的方式实现,而在 ApplicationContext 中则可以通过配置的方式实现。
ApplicationContext 继承了 HierarchicalBeanFactory 和 ListableBeanFactory 接口,在此基础上,还通过多个其他的接口扩展了 BeanFactory 的功能:
ClassPathXmlApplicationContext:默认从类路径加载配置文件
FileSystemXmlApplicationContext:默认从文件系统中装载配置文件
ApplicationEventPublisher:让容器拥有发布应用上下文事件的功能,包括容器启动事件、关闭事件等。实现了 ApplicationListener 事件监听接口的 Bean 可以接收到容器事件 , 并对事件进行响应处理 。 在 ApplicationContext 抽象实现类AbstractApplicationContext 中,我们可以发现存在一个 ApplicationEventMulticaster,它负责保存所有监听器,以便在容器产生上下文事件时通知这些事件监听者。
MessageSource:为应用提供 i18n 国际化消息访问的功能;
ResourcePatternResolver : 所 有 ApplicationContext 实现类都实现了类似于PathMatchingResourcePatternResolver 的功能,可以通过带前缀的 Ant 风格的资源文件路径装载 Spring 的配置文件。
LifeCycle:该接口是 Spring 2.0 加入的,该接口提供了 start()和 stop()两个方法,主要用于控制异步处理过程。在具体使用时,该接口同时被 ApplicationContext 实现及具体 Bean 实现, ApplicationContext 会将 start/stop 的信息传递给容器中所有实现了该接口的 Bean,以达到管理和控制 JMX、任务调度等目的。
ConfigurableApplicationContext 扩展于 ApplicationContext,它新增加了两个主要的方法: refresh()和 close(),让 ApplicationContext 具有启动、刷新和关闭应用上下文的能力。在应用上下文关闭的情况下调用 refresh()即可启动应用上下文,在已经启动的状态下,调用 refresh()则清除缓存并重新装载配置信息,而调用close()则可关闭应用上下文。这些接口方法为容器的控制管理带来了便利,但作为开发者,我们并不需要过多关心这些方法。
使用:
如果配置文件放置在类路径下,用户可以优先使用 ClassPathXmlApplicationContext 实现类:
如果配置文件放置在文件系统的路径下,则可以优先考虑使用 FileSystemXmlApplicationContext 实现类:
Spring 3.0 支持基于类注解的配置方式,主要功能来自于 Spring 的一个名为 JavaConfig 子项目,目前 JavaConfig已经升级为 Spring核心框架的一部分。
ApplicationContext 在初始化应用上下文时就实例化所有单实例的 Bean。
WebApplicationContext
WebApplication体系架构:
WebApplicationContext 是专门为 Web 应用准备的,它允许从相对于 Web 根目录的路径中装载配置文件完成初始化工作。从WebApplicationContext 中可以获得 ServletContext 的引用,整个 Web 应用上下文对象将作为属性放置到 ServletContext 中,以便 Web 应用环境可以访问 Spring 应用上下文。 WebApplicationContext 定义了一个常量ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,在上下文启动时, WebApplicationContext 实例即以此为键放置在 ServletContext 的属性列表中,因此我们可以直接通过以下语句从 Web 容器中获取WebApplicationContext:
Spring 和 Web 应用的上下文融合:
WebApplicationContext 的初始化方式:WebApplicationContext 需要 ServletContext 实例,它必须在拥有 Web 容器的前提下才能完成启动的工作。可以在 web.xml 中配置自启动的 Servlet 或定义 Web 容器监听器( ServletContextListener),借助这两者中的任何一个就可以完成启动 Spring Web 应用上下文的工作。Spring 分别提供了用于启动 WebApplicationContext 的 Servlet 和 Web 容器监听器:
org.springframework.web.context.ContextLoaderServlet;
org.springframework.web.context.ContextLoaderListener
由于 WebApplicationContext 需要使用日志功能,比如日志框架使用Log4J,用户可以将 Log4J 的配置文件放置到类路径 WEB-INF/classes 下,这时 Log4J 引擎即可顺利启动。如果 Log4J 配置文件放置在其他位置,用户还必须在 web.xml 指定 Log4J 配置文件位置。
Bean的生命周期
1.当调用者通过 getBean(beanName)向容器请求某一个 Bean 时,如果容器注册了org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor 接口,在实例化 Bean 之前,将调用接口的 postProcessBeforeInstantiation()方法;
2.根据配置情况调用 Bean 构造函数或工厂方法实例化 Bean;
3.如果容器注册了 InstantiationAwareBeanPostProcessor 接口,在实例化 Bean 之后,调用该接口的 postProcessAfterInstantiation()方法,可在这里对已经实例化的对象进行一些“梳妆打扮”;
4.如果 Bean 配置了属性信息,容器在这一步着手将配置值设置到 Bean 对应的属性中,不过在设置每个属性之前将先调用InstantiationAwareBeanPostProcessor 接口的postProcessPropertyValues()方法;
5.调用 Bean 的属性设置方法设置属性值;
6.如果 Bean 实现了 org.springframework.beans.factory.BeanNameAware 接口,将调用setBeanName()接口方法,将配置文件中该 Bean 对应的名称设置到 Bean 中;
7.如果 Bean 实现了 org.springframework.beans.factory.BeanFactoryAware 接口,将调用 setBeanFactory()接口方法,将 BeanFactory 容器实例设置到 Bean 中;
8.如果 BeanFactory 装配了 org.springframework.beans.factory.config.BeanPostProcessor后处理器,将调用 BeanPostProcessor 的 Object postProcessBeforeInitialization(Object bean, String beanName)接口方法对 Bean 进行加工操作。其中入参 bean 是当前正在处理的 Bean,而 beanName 是当前 Bean 的配置名,返回的对象为加工处理后的 Bean。用户可以使用该方法对某些 Bean 进行特殊的处理,甚至改变 Bean 的行为, BeanPostProcessor 在 Spring 框架中占有重要的地位,为容器提供对 Bean 进行后续加工处理的切入点, Spring 容器所提供的各种“神奇功能”(如 AOP,动态代理等)都通过 BeanPostProcessor 实施;
9.如果 Bean 实现了 InitializingBean 的接口,将调用接口的 afterPropertiesSet()方法;
10.如果在通过 init-method 属性定义了初始化方法,将执行这个方法;
11.BeanPostProcessor 后处理器定义了两个方法:其一是 postProcessBeforeInitialization() 在第 8 步调用;其二是 Object postProcessAfterInitialization(Object bean, String beanName)方法,这个方法在此时调用,容器再次获得对 Bean 进行加工处理的机会;
12.如果在中指定 Bean 的作用范围为 scope=“prototype”,将 Bean 返回给调用者,调用者负责 Bean 后续生命的管理, Spring 不再管理这个 Bean 的生命周期。如果作用范围设置为 scope=“singleton”,则将 Bean 放入到 Spring IoC 容器的缓存池中,并将 Bean引用返回给调用者, Spring 继续对这些 Bean 进行后续的生命管理;
13.对于 scope=“singleton”的 Bean,当容器关闭时,将触发 Spring 对 Bean 的后续生命周期的管理工作,首先如果 Bean 实现了 DisposableBean 接口,则将调用接口的afterPropertiesSet()方法,可以在此编写释放资源、记录日志等操作;
14.对于 scope=“singleton”的 Bean,如果通过的 destroy-method 属性指定了 Bean 的销毁方法, Spring 将执行 Bean 的这个方法,完成 Bean 资源的释放等操作。
可以将这些方法大致划分为三类:
Bean 自身的方法:如调用 Bean 构造函数实例化 Bean,调用 Setter 设置 Bean 的属性值以及通过的 init-method 和 destroy-method 所指定的方法;
Bean 级生命周期接口方法:如 BeanNameAware、 BeanFactoryAware、 InitializingBean 和 DisposableBean,这些接口方法由 Bean 类直接实现;
容器级生命周期接口方法:在上图中带“★” 的步骤是由 InstantiationAwareBean PostProcessor 和BeanPostProcessor 这两个接口实现,一般称它们的实现类为“ 后处理器” 。 后处理器接口一般不由 Bean 本身实现,它们独立于 Bean,实现类以容器附加装置的形式注册到 Spring 容器中并通过接口反射为 Spring 容器预先识别。当Spring 容器创建任何 Bean 的时候,这些后处理器都会发生作用,所以这些后处理器的影响是全局性的。当然,用户可以通过合理地编写后处理器,让其仅对感兴趣Bean 进行加工处理
ApplicationContext 和 BeanFactory 另一个最大的不同之处在于:ApplicationContext会利用 Java 反射机制自动识别出配置文件中定义的 BeanPostProcessor、 InstantiationAwareBeanPostProcessor 和 BeanFactoryPostProcessor,并自动将它们注册到应用上下文中;而后者需要在代码中通过手工调用 addBeanPostProcessor()方法进行注册。这也是为什么在应用开发时,我们普遍使用 ApplicationContext 而很少使用 BeanFactory 的原因之一
IOC容器工作机制
容器启动过程
web环境下Spring容器、SpringMVC容器启动过程:
首先,对于一个web应用,其部署在web容器中,web容器提供其一个全局的上下文环境,这个上下文就是ServletContext,其为后面的spring IoC容器提供宿主环境;
其次,在web.xml中会提供有contextLoaderListener(或ContextLoaderServlet)。在web容器启动时,会触发容器初始化事件,此时contextLoaderListener会监听到这个事件,其contextInitialized方法会被调用,在这个方法中,spring会初始化一个启动上下文,这个上下文被称为根上下文,即WebApplicationContext,这是一个接口类,确切的说,其实际的实现类是XmlWebApplicationContext。这个就是spring的IoC容器,其对应的Bean定义的配置由web.xml中的context-param标签指定。在这个IoC容器初始化完毕后,spring容器以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE为属性Key,将其存储到ServletContext中,便于获取;
再次,contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这个servlet可以配置多个,以最常见的DispatcherServlet为例(Spring MVC),这个servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请求。DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文容器,用以持有spring mvc相关的bean,这个servlet自己持有的上下文默认实现类也是XmlWebApplicationContext。在建立DispatcherServlet自己的IoC上下文时,会利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先从ServletContext中获取之前的根上下文(即WebApplicationContext)作为自己上下文的parent上下文(即第2步中初始化的XmlWebApplicationContext作为自己的父容器)。有了这个parent上下文之后,再初始化自己持有的上下文(这个DispatcherServlet初始化自己上下文的工作在其initStrategies方法中可以看到,大概的工作就是初始化处理器映射、视图解析等)。初始化完毕后,spring以与servlet的名字相关(此处不是简单的以servlet名为Key,而是通过一些转换)的属性为属性Key,也将其存到ServletContext中,以便后续使用。这样每个servlet就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的bean,即根上下文定义的那些bean。
Bean加载过程
Spring的高明之处在于,它使用众多接口描绘出了所有装置的蓝图,构建好Spring的骨架,继而通过继承体系层层推演,不断丰富,最终让Spring成为有血有肉的完整的框架。所以查看Spring框架的源码时,有两条清晰可见的脉络:
1)接口层描述了容器的重要组件及组件间的协作关系;
2)继承体系逐步实现组件的各项功能。
接口层清晰地勾勒出Spring框架的高层功能,框架脉络呼之欲出。有了接口层抽象的描述后,不但Spring自己可以提供具体的实现,任何第三方组织也可以提供不同实现, 可以说Spring完善的接口层使框架的扩展性得到了很好的保证。纵向继承体系的逐步扩展,分步骤地实现框架的功能,这种实现方案保证了框架功能不会堆积在某些类的身上,造成过重的代码逻辑负载,框架的复杂度被完美地分解开了。
Spring组件按其所承担的角色可以划分为两类:
1)物料组件:Resource、BeanDefinition、PropertyEditor以及最终的Bean等,它们是加工流程中被加工、被消费的组件,就像流水线上被加工的物料;
BeanDefinition:Spring通过BeanDefinition将配置文件中的配置信息转换为容器的内部表示,并将这些BeanDefinition注册到BeanDefinitionRegistry中。Spring容器的后续操作直接从BeanDefinitionRegistry中读取配置信息。
2)加工设备组件:ResourceLoader、BeanDefinitionReader、BeanFactoryPostProcessor、InstantiationStrategy以及BeanWrapper等组件像是流水线上不同环节的加工设备,对物料组件进行加工处理。
InstantiationStrategy:负责实例化Bean操作,相当于Java语言中new的功能,并不会参与Bean属性的配置工作。属性填充工作留待BeanWrapper完成
BeanWrapper:继承了PropertyAccessor和PropertyEditorRegistry接口,BeanWrapperImpl内部封装了两类组件:(1)被封装的目标Bean(2)一套用于设置Bean属性的属性编辑器;具有三重身份:(1)Bean包裹器(2)属性访问器 (3)属性编辑器注册表。PropertyAccessor:定义了各种访问Bean属性的方法。PropertyEditorRegistry:属性编辑器的注册表
该图描述了Spring容器从加载配置文件到创建出一个完整Bean的作业流程:
1、ResourceLoader从存储介质中加载Spring配置信息,并使用Resource表示这个配置文件的资源;
2、BeanDefinitionReader读取Resource所指向的配置文件资源,然后解析配置文件。配置文件中每一个解析成一个BeanDefinition对象,并保存到BeanDefinitionRegistry中;
3、容器扫描BeanDefinitionRegistry中的BeanDefinition,使用Java的反射机制自动识别出Bean工厂后处理后器(实现BeanFactoryPostProcessor接口)的Bean,然后调用这些Bean工厂后处理器对BeanDefinitionRegistry中的BeanDefinition进行加工处理。主要完成以下两项工作:
1)对使用到占位符的元素标签进行解析,得到最终的配置值,这意味对一些半成品式的BeanDefinition对象进行加工处理并得到成品的BeanDefinition对象;
2)对BeanDefinitionRegistry中的BeanDefinition进行扫描,通过Java反射机制找出所有属性编辑器的Bean(实现java.beans.PropertyEditor接口的Bean),并自动将它们注册到Spring容器的属性编辑器注册表中(PropertyEditorRegistry);
4.Spring容器从BeanDefinitionRegistry中取出加工后的BeanDefinition,并调用InstantiationStrategy着手进行Bean实例化的工作;
5.在实例化Bean时,Spring容器使用BeanWrapper对Bean进行封装,BeanWrapper提供了很多以Java反射机制操作Bean的方法,它将结合该Bean的BeanDefinition以及容器中属性编辑器,完成Bean属性的设置工作;
6.利用容器中注册的Bean后处理器(实现BeanPostProcessor接口的Bean)对已经完成属性设置工作的Bean进行后续加工,直接装配出一个准备就绪的Bean。
总结
Spring IOC容器主要有继承体系底层的BeanFactory、高层的ApplicationContext和WebApplicationContext
Bean有自己的生命周期
容器启动原理:Spring应用的IOC容器通过tomcat的Servlet或Listener监听启动加载;Spring MVC的容器由DispatchServlet作为入口加载;Spring容器是Spring MVC容器的父容器
容器加载Bean原理:
BeanDefinitionReader读取Resource所指向的配置文件资源,然后解析配置文件。配置文件中每一个解析成一个BeanDefinition对象,并保存到BeanDefinitionRegistry中;
容器扫描BeanDefinitionRegistry中的BeanDefinition;调用InstantiationStrategy进行Bean实例化的工作;使用BeanWrapper完成Bean属性的设置工作;
单例Bean缓存池:Spring 在 DefaultSingletonBeanRegistry 类中提供了一个用于缓存单实例 Bean 的缓存器,它是一个用 ConcurrentHashMap(存储懒汉式Bean)和HashMap(存储饿汉式Bean) 实现的缓存器,单实例的 Bean 以 beanName 为键保存在这个Map 中。

老生常谈的东西
spring的aop有两种实现(都是动态代理,没有静态代理):
一种是基于jdk动态代理:在被代理对象所属的类实现了接口的情况下使用,高优先级,代理对象是一个实现了接口的对象,和被代理对象是组合关系。
一种是基于CGLIG的代理:在被代理对象所属的类没有实现接口的情况下使用,低优先级,代理对象是被代理对象的子类。
原文
地址:
【Spring源码分析】AOP源码解析(上篇) http://www.cnblogs.com/xrq730/p/6753160.html
【Spring源码分析】AOP源码解析(下篇):  http://www.cnblogs.com/xrq730/p/6757608.html

【Spring源码分析】AOP源码解析(上篇)

前言
前面写了六篇文章详细地分析了Spring Bean加载流程,这部分完了之后就要进入一个比较困难的部分了,就是AOP的实现原理分析。为了探究AOP实现原理,首先定义几个类,一个Dao接口:
1 public interface Dao {
2
3 public void select();
4
5 public void insert();
6
7 }
Dao接口的实现类DaoImpl:
1 public class DaoImpl implements Dao {
2
3 @Override
4 public void select() {
5 System.out.println("Enter DaoImpl.select()");
6 }
7
8 @Override
9 public void insert() {
10 System.out.println("Enter DaoImpl.insert()");
11 }
12
13 }
定义一个TimeHandler,用于方法调用前后打印时间,在AOP中,这扮演的是横切关注点的角色:
1 public class TimeHandler {
2
3 public void printTime() {
4 System.out.println("CurrentTime:" + System.currentTimeMillis());
5 }
6
7 }
定义一个XML文件aop.xml:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xmlns:tx="http://www.springframework.org/schema/tx" 6 xsi:schemaLocation="http://www.springframework.org/schema/beans
7 http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
8 http://www.springframework.org/schema/aop
9 http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
10
11 <bean id="daoImpl" class="org.xrq.action.aop.DaoImpl" />
12 <bean id="timeHandler" class="org.xrq.action.aop.TimeHandler" />
13
14 <aop:config proxy-target-class="true">
15 <aop:aspect id="time" ref="timeHandler">
16 <aop:pointcut id="addAllMethod" expression="execution(* org.xrq.action.aop.Dao.*(..))" />
17 <aop:before method="printTime" pointcut-ref="addAllMethod" />
18 <aop:after method="printTime" pointcut-ref="addAllMethod" />
19 </aop:aspect>
20 </aop:config>21
22 </beans>
写一段测试代码TestAop.java:
1 public class TestAop {
2
3 @Test
4 public void testAop() {
5 ApplicationContext ac = new ClassPathXmlApplicationContext("spring/aop.xml");
6
7 Dao dao = (Dao)ac.getBean("daoImpl");
8 dao.select();
9 }
10
11 }
代码运行结果就不看了,有了以上的内容,我们就可以根据这些跟一下代码,看看Spring到底是如何实现AOP的。
 
AOP实现原理----找到Spring处理AOP的源头
有很多朋友不愿意去看AOP源码的一个很大原因是因为找不到AOP源码实现的入口在哪里,这个确实是。不过我们可以看一下上面的测试代码,就普通Bean也好、AOP也好,最终都是通过getBean方法获取到Bean并调用方法的,getBean之后的对象已经前后都打印了TimeHandler类printTime()方法里面的内容,可以想见它们已经是被Spring容器处理过了。
既然如此,那无非就两个地方处理:
  1. 加载Bean定义的时候应该有过特殊的处理
  2. getBean的时候应该有过特殊的处理
因此,本文围绕【1.加载Bean定义的时候应该有过特殊的处理】展开,先找一下到底是哪里Spring对AOP做了特殊的处理。代码直接定位到DefaultBeanDefinitionDocumentReader的parseBeanDefinitions方法:
1 protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
2 if (delegate.isDefaultNamespace(root)) {
3 NodeList nl = root.getChildNodes();
4 for (int i = 0; i < nl.getLength(); i++) {
5 Node node = nl.item(i);
6 if (node instanceof Element) {
7 Element ele = (Element) node;
8 if (delegate.isDefaultNamespace(ele)) {
9 parseDefaultElement(ele, delegate);
10 }
11 else {
12 delegate.parseCustomElement(ele);
13 }
14 }
15 }
16 }
17 else {
18 delegate.parseCustomElement(root);
19 }
20 }
正常来说,遇到<bean id="daoImpl"...>、<bean id="timeHandler"...>这两个标签的时候,都会执行第9行的代码,因为<bean>标签是默认的Namespace。但是在遇到后面的<aop:config>标签的时候就不一样了,<aop:config>并不是默认的Namespace,因此会执行第12行的代码,看一下:
1 public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
2 String namespaceUri = getNamespaceURI(ele);
3 NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
4 if (handler == null) {
5 error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
6 return null;
7 }
8 return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
9 }
因为之前把整个XML解析为了org.w3c.dom.Document,org.w3c.dom.Document以树的形式表示整个XML,具体到每一个节点就是一个Node。
首先第2行从<aop:config>这个Node(参数Element是Node接口的子接口)中拿到Namespace="http://www.springframework.org/schema/aop",第3行的代码根据这个Namespace获取对应的NamespaceHandler即Namespace处理器,具体到aop这个Namespace的NamespaceHandler是org.springframework.aop.config.AopNamespaceHandler类,也就是第3行代码获取到的结果。具体到AopNamespaceHandler里面,有几个Parser,是用于具体标签转换的,分别为:
接着,就是第8行的代码,利用AopNamespaceHandler的parse方法,解析<aop:config>下的内容了。
 
AOP Bean定义加载----根据织入方式将<aop:before>、<aop:after>转换成名为adviceDef的RootBeanDefinition
上面经过分析,已经找到了Spring是通过AopNamespaceHandler处理的AOP,那么接着进入AopNamespaceHandler的parse方法源代码:
1 public BeanDefinition parse(Element element, ParserContext parserContext) {
2 return findParserForElement(element, parserContext).parse(element, parserContext);
3 }
首先获取具体的Parser,因为当前节点是<aop:config>,上一部分最后有列,config是通过ConfigBeanDefinitionParser来处理的,因此findParserForElement(element, parserContext)这一部分代码获取到的是ConfigBeanDefinitionParser,接着看ConfigBeanDefinitionParser的parse方法:
1 public BeanDefinition parse(Element element, ParserContext parserContext) {
2 CompositeComponentDefinition compositeDef =
3 new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element));
4 parserContext.pushContainingComponent(compositeDef);
5
6 configureAutoProxyCreator(parserContext, element);
7
8 List<Element> childElts = DomUtils.getChildElements(element);
9 for (Element elt: childElts) {
10 String localName = parserContext.getDelegate().getLocalName(elt);
11 if (POINTCUT.equals(localName)) {
12 parsePointcut(elt, parserContext);
13 }
14 else if (ADVISOR.equals(localName)) {
15 parseAdvisor(elt, parserContext);
16 }
17 else if (ASPECT.equals(localName)) {
18 parseAspect(elt, parserContext);
19 }
20 }
21
22 parserContext.popAndRegisterContainingComponent();
23 return null;
24 }
重点先提一下第6行的代码,该行代码的具体实现不跟了但它非常重要,configureAutoProxyCreator方法的作用我用几句话说一下:
<aop:config>下的节点为<aop:aspect>,想见必然是执行第18行的代码parseAspect,跟进去:
1 private void parseAspect(Element aspectElement, ParserContext parserContext) {
2 String aspectId = aspectElement.getAttribute(ID);
3 String aspectName = aspectElement.getAttribute(REF);
4
5 try {
6 this.parseState.push(new AspectEntry(aspectId, aspectName));
7 List<BeanDefinition> beanDefinitions = new ArrayList<BeanDefinition>();
8 List<BeanReference> beanReferences = new ArrayList<BeanReference>();
9
10 List<Element> declareParents = DomUtils.getChildElementsByTagName(aspectElement, DECLARE_PARENTS);
11 for (int i = METHOD_INDEX; i < declareParents.size(); i++) {
12 Element declareParentsElement = declareParents.get(i);
13 beanDefinitions.add(parseDeclareParents(declareParentsElement, parserContext));
14 }
15
16 // We have to parse "advice" and all the advice kinds in one loop, to get the
17 // ordering semantics right.18 NodeList nodeList = aspectElement.getChildNodes();
19 boolean adviceFoundAlready = false;
20 for (int i = 0; i < nodeList.getLength(); i++) {
21 Node node = nodeList.item(i);
22 if (isAdviceNode(node, parserContext)) {
23 if (!adviceFoundAlready) {
24 adviceFoundAlready = true;
25 if (!StringUtils.hasText(aspectName)) {
26 parserContext.getReaderContext().error(
27 "<aspect> tag needs aspect bean reference via 'ref' attribute when declaring advices.",
28 aspectElement, this.parseState.snapshot());
29 return;
30 }
31 beanReferences.add(new RuntimeBeanReference(aspectName));
32 }
33 AbstractBeanDefinition advisorDefinition = parseAdvice(
34 aspectName, i, aspectElement, (Element) node, parserContext, beanDefinitions, beanReferences);
35 beanDefinitions.add(advisorDefinition);
36 }
37 }
38
39 AspectComponentDefinition aspectComponentDefinition = createAspectComponentDefinition(
40 aspectElement, aspectId, beanDefinitions, beanReferences, parserContext);
41 parserContext.pushContainingComponent(aspectComponentDefinition);
42
43 List<Element> pointcuts = DomUtils.getChildElementsByTagName(aspectElement, POINTCUT);
44 for (Element pointcutElement : pointcuts) {
45 parsePointcut(pointcutElement, parserContext);
46 }
47
48 parserContext.popAndRegisterContainingComponent();
49 }
50 finally {
51 this.parseState.pop();
52 }
53 }
从第20行~第37行的循环开始关注这个方法。这个for循环有一个关键的判断就是第22行的ifAdviceNode判断,看下ifAdviceNode方法做了什么:
1 private boolean isAdviceNode(Node aNode, ParserContext parserContext) {
2 if (!(aNode instanceof Element)) {
3 return false;
4 }
5 else {
6 String name = parserContext.getDelegate().getLocalName(aNode);
7 return (BEFORE.equals(name) || AFTER.equals(name) || AFTER_RETURNING_ELEMENT.equals(name) ||
8 AFTER_THROWING_ELEMENT.equals(name) || AROUND.equals(name));
9 }
10 }
即这个for循环只用来处理<aop:aspect>标签下的<aop:before>、<aop:after>、<aop:after-returning>、<aop:after-throwing method="">、<aop:around method="">这五个标签的。
接着,如果是上述五种标签之一,那么进入第33行~第34行的parseAdvice方法:
1 private AbstractBeanDefinition parseAdvice(
2 String aspectName, int order, Element aspectElement, Element adviceElement, ParserContext parserContext,
3 List<BeanDefinition> beanDefinitions, List<BeanReference> beanReferences) {
4 5 try {
6 this.parseState.push(new AdviceEntry(parserContext.getDelegate().getLocalName(adviceElement)));
7 8 // create the method factory bean 9 RootBeanDefinition methodDefinition = new RootBeanDefinition(MethodLocatingFactoryBean.class);
10 methodDefinition.getPropertyValues().add("targetBeanName", aspectName);
11 methodDefinition.getPropertyValues().add("methodName", adviceElement.getAttribute("method"));
12 methodDefinition.setSynthetic(true);
1314 // create instance factory definition15 RootBeanDefinition aspectFactoryDef =
16 new RootBeanDefinition(SimpleBeanFactoryAwareAspectInstanceFactory.class);
17 aspectFactoryDef.getPropertyValues().add("aspectBeanName", aspectName);
18 aspectFactoryDef.setSynthetic(true);
19
20 // register the pointcut21 AbstractBeanDefinition adviceDef = createAdviceDefinition(
22 adviceElement, parserContext, aspectName, order, methodDefinition, aspectFactoryDef,
23 beanDefinitions, beanReferences);
24
25 // configure the advisor26 RootBeanDefinition advisorDefinition = new RootBeanDefinition(AspectJPointcutAdvisor.class);
27 advisorDefinition.setSource(parserContext.extractSource(adviceElement));
28 advisorDefinition.getConstructorArgumentValues().addGenericArgumentValue(adviceDef);
29 if (aspectElement.hasAttribute(ORDER_PROPERTY)) {
30 advisorDefinition.getPropertyValues().add(
31 ORDER_PROPERTY, aspectElement.getAttribute(ORDER_PROPERTY));
32 }
33
34 // register the final advisor35 parserContext.getReaderContext().registerWithGeneratedName(advisorDefinition);
36
37 return advisorDefinition;
38 }
39 finally {
40 this.parseState.pop();
41 }
42 }
方法主要做了三件事:
  1. 根据织入方式(before、after这些)创建RootBeanDefinition,名为adviceDef即advice定义
  2. 将上一步创建的RootBeanDefinition写入一个新的RootBeanDefinition,构造一个新的对象,名为advisorDefinition,即advisor定义
  3. 将advisorDefinition注册到DefaultListableBeanFactory中
下面来看做的第一件事createAdviceDefinition方法定义:
1 private AbstractBeanDefinition createAdviceDefinition(
2 Element adviceElement, ParserContext parserContext, String aspectName, int order,
3 RootBeanDefinition methodDef, RootBeanDefinition aspectFactoryDef,
4 List<BeanDefinition> beanDefinitions, List<BeanReference> beanReferences) {
5
6 RootBeanDefinition adviceDefinition = new RootBeanDefinition(getAdviceClass(adviceElement, parserContext));
7 adviceDefinition.setSource(parserContext.extractSource(adviceElement));
8 adviceDefinition.getPropertyValues().add(ASPECT_NAME_PROPERTY, aspectName);
9 adviceDefinition.getPropertyValues().add(DECLARATION_ORDER_PROPERTY, order);
10
11 if (adviceElement.hasAttribute(RETURNING)) {
12 adviceDefinition.getPropertyValues().add(
13 RETURNING_PROPERTY, adviceElement.getAttribute(RETURNING));
14 }
15 if (adviceElement.hasAttribute(THROWING)) {
16 adviceDefinition.getPropertyValues().add(
17 THROWING_PROPERTY, adviceElement.getAttribute(THROWING));
18 }
19 if (adviceElement.hasAttribute(ARG_NAMES)) {
20 adviceDefinition.getPropertyValues().add(
21 ARG_NAMES_PROPERTY, adviceElement.getAttribute(ARG_NAMES));
22 }
23
24 ConstructorArgumentValues cav = adviceDefinition.getConstructorArgumentValues();
25 cav.addIndexedArgumentValue(METHOD_INDEX, methodDef);
26
27 Object pointcut = parsePointcutProperty(adviceElement, parserContext);
28 if (pointcut instanceof BeanDefinition) {
29 cav.addIndexedArgumentValue(POINTCUT_INDEX, pointcut);
30 beanDefinitions.add((BeanDefinition) pointcut);
31 }
32 else if (pointcut instanceof String) {
33 RuntimeBeanReference pointcutRef = new RuntimeBeanReference((String) pointcut);
34 cav.addIndexedArgumentValue(POINTCUT_INDEX, pointcutRef);
35 beanReferences.add(pointcutRef);
36 }
37
38 cav.addIndexedArgumentValue(ASPECT_INSTANCE_FACTORY_INDEX, aspectFactoryDef);
39
40 return adviceDefinition;
41 }
首先可以看到,创建的AbstractBeanDefinition实例是RootBeanDefinition,这和普通Bean创建的实例为GenericBeanDefinition不同。然后进入第6行的getAdviceClass方法看一下:
1 private Class getAdviceClass(Element adviceElement, ParserContext parserContext) {
2 String elementName = parserContext.getDelegate().getLocalName(adviceElement);
3 if (BEFORE.equals(elementName)) {
4 return AspectJMethodBeforeAdvice.class;
5 }
6 else if (AFTER.equals(elementName)) {
7 return AspectJAfterAdvice.class;
8 }
9 else if (AFTER_RETURNING_ELEMENT.equals(elementName)) {
10 return AspectJAfterReturningAdvice.class;
11 }
12 else if (AFTER_THROWING_ELEMENT.equals(elementName)) {
13 return AspectJAfterThrowingAdvice.class;
14 }
15 else if (AROUND.equals(elementName)) {
16 return AspectJAroundAdvice.class;
17 }
18 else {
19 throw new IllegalArgumentException("Unknown advice kind [" + elementName + "].");
20 }
21 }
既然创建Bean定义,必然该Bean定义中要对应一个具体的Class,不同的切入方式对应不同的Class:
createAdviceDefinition方法剩余逻辑没什么,就是判断一下标签里面的属性并设置一下相应的值而已,至此<aop:before>、<aop:after>两个标签对应的AbstractBeanDefinition就创建出来了。
 
AOP Bean定义加载----将名为adviceDef的RootBeanDefinition转换成名为advisorDefinition的RootBeanDefinition
下面我们看一下第二步的操作,将名为adviceDef的RootBeanD转换成名为advisorDefinition的RootBeanDefinition,跟一下上面一部分ConfigBeanDefinitionParser类parseAdvice方法的第26行~32行的代码:
1 RootBeanDefinition advisorDefinition = new RootBeanDefinition(AspectJPointcutAdvisor.class);
2 advisorDefinition.setSource(parserContext.extractSource(adviceElement));
3 advisorDefinition.getConstructorArgumentValues().addGenericArgumentValue(adviceDef);
4 if (aspectElement.hasAttribute(ORDER_PROPERTY)) {
5 advisorDefinition.getPropertyValues().add(
6 ORDER_PROPERTY, aspectElement.getAttribute(ORDER_PROPERTY));
7 }
这里相当于将上一步生成的RootBeanDefinition包装了一下,new一个新的RootBeanDefinition出来,Class类型是org.springframework.aop.aspectj.AspectJPointcutAdvisor。
第4行~第7行的代码是用于判断<aop:aspect>标签中有没有"order"属性的,有就设置一下,"order"属性是用来控制切入方法优先级的。
 
AOP Bean定义加载----将BeanDefinition注册到DefaultListableBeanFactory中
最后一步就是将BeanDefinition注册到DefaultListableBeanFactory中了,代码就是前面ConfigBeanDefinitionParser的parseAdvice方法的最后一部分了:
1 ...
2 // register the final advisor3 parserContext.getReaderContext().registerWithGeneratedName(advisorDefinition);
4 ...
跟一下registerWithGeneratedName方法的实现:
1 public String registerWithGeneratedName(BeanDefinition beanDefinition) {
2 String generatedName = generateBeanName(beanDefinition);
3 getRegistry().registerBeanDefinition(generatedName, beanDefinition);
4 return generatedName;
5 }
第2行获取注册的名字BeanName,和<bean>的注册差不多,使用的是Class全路径+"#"+全局计数器的方式,其中的Class全路径为org.springframework.aop.aspectj.AspectJPointcutAdvisor,依次类推,每一个BeanName应当为org.springframework.aop.aspectj.AspectJPointcutAdvisor#0、org.springframework.aop.aspectj.AspectJPointcutAdvisor#1、org.springframework.aop.aspectj.AspectJPointcutAdvisor#2这样下去。
第3行向DefaultListableBeanFactory中注册,BeanName已经有了,剩下的就是Bean定义,Bean定义的解析流程之前已经看过了,就不说了。
 
AOP Bean定义加载----AopNamespaceHandler处理<aop:pointcut>流程
回到ConfigBeanDefinitionParser的parseAspect方法:
1 private void parseAspect(Element aspectElement, ParserContext parserContext) {
2
3 ...
4
5 AspectComponentDefinition aspectComponentDefinition = createAspectComponentDefinition(
6 aspectElement, aspectId, beanDefinitions, beanReferences, parserContext);
7 parserContext.pushContainingComponent(aspectComponentDefinition);
8
9 List<Element> pointcuts = DomUtils.getChildElementsByTagName(aspectElement, POINTCUT);
10 for (Element pointcutElement : pointcuts) {
11 parsePointcut(pointcutElement, parserContext);
12 }
13
14 parserContext.popAndRegisterContainingComponent();
15 }
16 finally {
17 this.parseState.pop();
18 }
19 }
省略号部分表示是解析的是<aop:before>、<aop:after>这种标签,上部分已经说过了,就不说了,下面看一下解析<aop:pointcut>部分的源码。
第5行~第7行的代码构建了一个Aspect标签组件定义,并将Apsect标签组件定义推到ParseContext即解析工具上下文中,这部分代码不是关键。
第9行的代码拿到所有<aop:aspect>下的pointcut标签,进行遍历,由parsePointcut方法进行处理:
1 private AbstractBeanDefinition parsePointcut(Element pointcutElement, ParserContext parserContext) {
2 String id = pointcutElement.getAttribute(ID);
3 String expression = pointcutElement.getAttribute(EXPRESSION);
4
5 AbstractBeanDefinition pointcutDefinition = null;
6
7 try {
8 this.parseState.push(new PointcutEntry(id));
9 pointcutDefinition = createPointcutDefinition(expression);
10 pointcutDefinition.setSource(parserContext.extractSource(pointcutElement));
11
12 String pointcutBeanName = id;
13 if (StringUtils.hasText(pointcutBeanName)) {
14 parserContext.getRegistry().registerBeanDefinition(pointcutBeanName, pointcutDefinition);
15 }
16 else {
17 pointcutBeanName = parserContext.getReaderContext().registerWithGeneratedName(pointcutDefinition);
18 }
19
20 parserContext.registerComponent(
21 new PointcutComponentDefinition(pointcutBeanName, pointcutDefinition, expression));
22 }
23 finally {
24 this.parseState.pop();
25 }
26
27 return pointcutDefinition;
28 }
第2行~第3行的代码获取<aop:pointcut>标签下的"id"属性与"expression"属性。
第8行的代码推送一个PointcutEntry,表示当前Spring上下文正在解析Pointcut标签。
第9行的代码创建Pointcut的Bean定义,之后再看,先把其他方法都看一下。
第10行的代码不管它,最终从NullSourceExtractor的extractSource方法获取Source,就是个null。
第12行~第18行的代码用于注册获取到的Bean定义,默认pointcutBeanName为<aop:pointcut>标签中定义的id属性:
第20行~第21行的代码向解析工具上下文中注册一个Pointcut组件定义
第23行~第25行的代码,finally块在<aop:pointcut>标签解析完毕后,让之前推送至栈顶的PointcutEntry出栈,表示此次<aop:pointcut>标签解析完毕。
最后回头来一下第9行代码createPointcutDefinition的实现,比较简单:
1 protected AbstractBeanDefinition createPointcutDefinition(String expression) {
2 RootBeanDefinition beanDefinition = new RootBeanDefinition(AspectJExpressionPointcut.class);
3 beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
4 beanDefinition.setSynthetic(true);
5 beanDefinition.getPropertyValues().add(EXPRESSION, expression);
6 return beanDefinition;
7 }
关键就是注意一下两点:
  1. <aop:pointcut>标签对应解析出来的BeanDefinition是RootBeanDefinition,且RootBenaDefinitoin中的Class是org.springframework.aop.aspectj.AspectJExpressionPointcut
  2. <aop:pointcut>标签对应的Bean是prototype即原型的
这样一个流程下来,就解析了<aop:pointcut>标签中的内容并将之转换为RootBeanDefintion存储在Spring容器中。
 
================================================================================== 

我不能保证写的每个地方都是对的,但是至少能保证不复制、不黏贴,保证每一句话、每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后,希望都能看到自己对于技术、对于生活的态度。

我相信乔布斯说的,只有那些疯狂到认为自己可以改变世界的人才能真正地改变世界。面对压力,我可以挑灯夜战、不眠不休;面对困难,我愿意迎难而上、永不退缩。

其实我想说的是,我只是一个程序员,这就是我现在纯粹人生的全部。

==================================================================================

【Spring源码分析】AOP源码解析(下篇)

AspectJAwareAdvisorAutoProxyCreator及为Bean生成代理时机分析
上篇文章说了,org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator这个类是Spring提供给开发者的AOP的核心类,就是AspectJAwareAdvisorAutoProxyCreator完成了【类/接口-->代理】的转换过程,首先我们看一下AspectJAwareAdvisorAutoProxyCreator的层次结构:
这里最值得注意的一点是最左下角的那个方框,我用几句话总结一下:
  1. AspectJAwareAdvisorAutoProxyCreator是BeanPostProcessor接口的实现类
  2. postProcessBeforeInitialization方法与postProcessAfterInitialization方法实现在父类AbstractAutoProxyCreator
  3. postProcessBeforeInitialization方法是一个空实现
  4. 逻辑代码在postProcessAfterInitialization方法中
基于以上的分析,将Bean生成代理的时机已经一目了然了:在每个Bean初始化之后,如果需要,调用AspectJAwareAdvisorAutoProxyCreator中的postProcessBeforeInitialization为Bean生成代理
 
代理对象实例化----判断是否为<bean>生成代理
上文分析了Bean生成代理的时机是在每个Bean初始化之后,下面把代码定位到Bean初始化之后,先是AbstractAutowireCapableBeanFactory的initializeBean方法进行初始化:
1 protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
2 if (System.getSecurityManager() != null) {
3 AccessController.doPrivileged(new PrivilegedAction<Object>() {
4 public Object run() {
5 invokeAwareMethods(beanName, bean);
6 return null;
7 }
8 }, getAccessControlContext());
9 }
10 else {
11 invokeAwareMethods(beanName, bean);
12 }
13
14 Object wrappedBean = bean;
15 if (mbd == null || !mbd.isSynthetic()) {
16 wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
17 }
18
19 try {
20 invokeInitMethods(beanName, wrappedBean, mbd);
21 }
22 catch (Throwable ex) {
23 throw new BeanCreationException(
24 (mbd != null ? mbd.getResourceDescription() : null),
25 beanName, "Invocation of init method failed", ex);
26 }
27
28 if (mbd == null || !mbd.isSynthetic()) {
29 wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
30 }
31 return wrappedBean;
32 }
初始化之前是第16行的applyBeanPostProcessorsBeforeInitialization方法,初始化之后即29行的applyBeanPostProcessorsAfterInitialization方法:
1 public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
2 throws BeansException {
3
4 Object result = existingBean;
5 for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
6 result = beanProcessor.postProcessAfterInitialization(result, beanName);
7 if (result == null) {
8 return result;
9 }
10 }
11 return result;
12 }
这里调用每个BeanPostProcessor的postProcessBeforeInitialization方法。按照之前的分析,看一下AbstractAutoProxyCreator的postProcessAfterInitialization方法实现:
1 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
2 if (bean != null) {
3 Object cacheKey = getCacheKey(bean.getClass(), beanName);
4 if (!this.earlyProxyReferences.contains(cacheKey)) {
5 return wrapIfNecessary(bean, beanName, cacheKey);
6 }
7 }
8 return bean;
9 }
跟一下第5行的方法wrapIfNecessary:
1 protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
2 if (this.targetSourcedBeans.contains(beanName)) {
3 return bean;
4 }
5 if (this.nonAdvisedBeans.contains(cacheKey)) {
6 return bean;
7 }
8 if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
9 this.nonAdvisedBeans.add(cacheKey);
10 return bean;
11 }
12
13 // Create proxy if we have advice.14 Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
15 if (specificInterceptors != DO_NOT_PROXY) {
16 this.advisedBeans.add(cacheKey);
17 Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
18 this.proxyTypes.put(cacheKey, proxy.getClass());
19 return proxy;
20 }
21
22 this.nonAdvisedBeans.add(cacheKey);
23 return bean;
24 }
第2行~第11行是一些不需要生成代理的场景判断,这里略过。首先我们要思考的第一个问题是:哪些目标对象需要生成代理因为配置文件里面有很多Bean,肯定不能对每个Bean都生成代理,因此需要一套规则判断Bean是不是需要生成代理,这套规则就是第14行的代码getAdvicesAndAdvisorsForBean:
1 protected List<Advisor> findEligibleAdvisors(Class beanClass, String beanName) {
2 List<Advisor> candidateAdvisors = findCandidateAdvisors();
3 List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
4 extendAdvisors(eligibleAdvisors);
5 if (!eligibleAdvisors.isEmpty()) {
6 eligibleAdvisors = sortAdvisors(eligibleAdvisors);
7 }
8 return eligibleAdvisors;
9 }
顾名思义,方法的意思是为指定class寻找合适的Advisor。
第2行代码,寻找候选Advisors,根据上文的配置文件,有两个候选Advisor,分别是<aop:aspect>节点下的<aop:before>和<aop:after>这两个,这两个在XML解析的时候已经被转换生成了RootBeanDefinition。
跳过第3行的代码,先看下第4行的代码extendAdvisors方法,之后再重点看一下第3行的代码。第4行的代码extendAdvisors方法作用是向候选Advisor链的开头(也就是List.get(0)的位置)添加一个org.springframework.aop.support.DefaultPointcutAdvisor
第3行代码,根据候选Advisors,寻找可以使用的Advisor,跟一下方法实现:
1 public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
2 if (candidateAdvisors.isEmpty()) {
3 return candidateAdvisors;
4 }
5 List<Advisor> eligibleAdvisors = new LinkedList<Advisor>();
6 for (Advisor candidate : candidateAdvisors) {
7 if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
8 eligibleAdvisors.add(candidate);
9 }
10 }
11 boolean hasIntroductions = !eligibleAdvisors.isEmpty();
12 for (Advisor candidate : candidateAdvisors) {
13 if (candidate instanceof IntroductionAdvisor) {
14 // already processed15 continue;
16 }
17 if (canApply(candidate, clazz, hasIntroductions)) {
18 eligibleAdvisors.add(candidate);
19 }
20 }
21 return eligibleAdvisors;
22 }
整个方法的主要判断都围绕canApply展开方法:
1 public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
2 if (advisor instanceof IntroductionAdvisor) {
3 return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
4 }
5 else if (advisor instanceof PointcutAdvisor) {
6 PointcutAdvisor pca = (PointcutAdvisor) advisor;
7 return canApply(pca.getPointcut(), targetClass, hasIntroductions);
8 }
9 else {
10 // It doesn't have a pointcut so we assume it applies.11 return true;
12 }
13 }
第一个参数advisor的实际类型是AspectJPointcutAdvisor,它是PointcutAdvisor的子类,因此执行第7行的方法:
1 public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
2 if (!pc.getClassFilter().matches(targetClass)) {
3 return false;
4 }
5
6 MethodMatcher methodMatcher = pc.getMethodMatcher();
7 IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
8 if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
9 introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
10 }
11
12 Set<Class> classes = new HashSet<Class>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
13 classes.add(targetClass);
14 for (Class<?> clazz : classes) {
15 Method[] methods = clazz.getMethods();
16 for (Method method : methods) {
17 if ((introductionAwareMethodMatcher != null &&
18 introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
19 methodMatcher.matches(method, targetClass)) {
20 return true;
21 }
22 }
23 }
24 return false;
25 }
这个方法其实就是拿当前Advisor对应的expression做了两层判断:
  1. 目标类必须满足expression的匹配规则
  2. 目标类中的方法必须满足expression的匹配规则,当然这里方法不是全部需要满足expression的匹配规则,有一个方法满足即可
如果以上两条都满足,那么容器则会判断该<bean>满足条件,需要被生成代理对象,具体方式为返回一个数组对象,该数组对象中存储的是<bean>对应的Advisor。
 
代理对象实例化----为<bean>生成代理代码上下文梳理
上文分析了为<bean>生成代理的条件,现在就正式看一下Spring上下文是如何为<bean>生成代理的。回到AbstractAutoProxyCreator的wrapIfNecessary方法:
1 protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
2 if (this.targetSourcedBeans.contains(beanName)) {
3 return bean;
4 }
5 if (this.nonAdvisedBeans.contains(cacheKey)) {
6 return bean;
7 }
8 if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
9 this.nonAdvisedBeans.add(cacheKey);
10 return bean;
11 }
12
13 // Create proxy if we have advice.14 Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
15 if (specificInterceptors != DO_NOT_PROXY) {
16 this.advisedBeans.add(cacheKey);
17 Object proxy = createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
18 this.proxyTypes.put(cacheKey, proxy.getClass());
19 return proxy;
20 }
21
22 this.nonAdvisedBeans.add(cacheKey);
23 return bean;
24 }
第14行拿到<bean>对应的Advisor数组,第15行判断只要Advisor数组不为空,那么就会通过第17行的代码为<bean>创建代理:
1 protected Object createProxy(
2 Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {
3
4 ProxyFactory proxyFactory = new ProxyFactory();
5 // Copy our properties (proxyTargetClass etc) inherited from ProxyConfig. 6 proxyFactory.copyFrom(this);
7
8 if (!shouldProxyTargetClass(beanClass, beanName)) {
9 // Must allow for introductions; can't just set interfaces to
10 // the target's interfaces only.11 Class<?>[] targetInterfaces = ClassUtils.getAllInterfacesForClass(beanClass, this.proxyClassLoader);
12 for (Class<?> targetInterface : targetInterfaces) {
13 proxyFactory.addInterface(targetInterface);
14 }
15 }
16
17 Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
18 for (Advisor advisor : advisors) {
19 proxyFactory.addAdvisor(advisor);
20 }
21
22 proxyFactory.setTargetSource(targetSource);
23 customizeProxyFactory(proxyFactory);
24
25 proxyFactory.setFrozen(this.freezeProxy);
26 if (advisorsPreFiltered()) {
27 proxyFactory.setPreFiltered(true);
28 }
29
30 return proxyFactory.getProxy(this.proxyClassLoader);
31 }
第4行~第6行new出了一个ProxyFactory,Proxy,顾名思义,代理工厂的意思,提供了简单的方式使用代码获取和配置AOP代理。
第8行的代码做了一个判断,判断的内容是<aop:config>这个节点中proxy-target-class="false"或者proxy-target-class不配置,即不使用CGLIB生成代理。如果满足条件,进判断,获取当前Bean实现的所有接口,讲这些接口Class对象都添加到ProxyFactory中。
第17行~第28行的代码没什么看的必要,向ProxyFactory中添加一些参数而已。重点看第30行proxyFactory.getProxy(this.proxyClassLoader)这句:
1 public Object getProxy(ClassLoader classLoader) {
2 return createAopProxy().getProxy(classLoader);
3 }
实现代码就一行,但是却明确告诉我们做了两件事情:
  1. 创建AopProxy接口实现类
  2. 通过AopProxy接口的实现类的getProxy方法获取<bean>对应的代理
就从这两个点出发,分两部分分析一下。
 
代理对象实例化----创建AopProxy接口实现类
看一下createAopProxy()方法的实现,它位于DefaultAopProxyFactory类中:
1 protected final synchronized AopProxy createAopProxy() {
2 if (!this.active) {
3 activate();
4 }
5 return getAopProxyFactory().createAopProxy(this);
6 }
前面的部分没什么必要看,直接进入重点即createAopProxy方法:
1 public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
2 if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
3 Class targetClass = config.getTargetClass();
4 if (targetClass == null) {
5 throw new AopConfigException("TargetSource cannot determine target class: " +
6 "Either an interface or a target is required for proxy creation.");
7 }
8 if (targetClass.isInterface()) {
9 return new JdkDynamicAopProxy(config);
10 }
11 if (!cglibAvailable) {
12 throw new AopConfigException(
13 "Cannot proxy target class because CGLIB2 is not available. " +
14 "Add CGLIB to the class path or specify proxy interfaces.");
15 }
16 return CglibProxyFactory.createCglibProxy(config);
17 }
18 else {
19 return new JdkDynamicAopProxy(config);
20 }
21 }
平时我们说AOP原理三句话就能概括:
  1. 对类生成代理使用CGLIB
  2. 对接口生成代理使用JDK原生的Proxy
  3. 可以通过配置文件指定对接口使用CGLIB生成代理
这三句话的出处就是createAopProxy方法。看到默认是第19行的代码使用JDK自带的Proxy生成代理,碰到以下三种情况例外:
  1. ProxyConfig的isOptimize方法为true,这表示让Spring自己去优化而不是用户指定
  2. ProxyConfig的isProxyTargetClass方法为true,这表示配置了proxy-target-class="true"
  3. ProxyConfig满足hasNoUserSuppliedProxyInterfaces方法执行结果为true,这表示<bean>对象没有实现任何接口或者实现的接口是SpringProxy接口
在进入第2行的if判断之后再根据目标<bean>的类型决定返回哪种AopProxy。简单总结起来就是:
  1. proxy-target-class没有配置或者proxy-target-class="false",返回JdkDynamicAopProxy
  2. proxy-target-class="true"或者<bean>对象没有实现任何接口或者只实现了SpringProxy接口,返回Cglib2AopProxy
当然,不管是JdkDynamicAopProxy还是Cglib2AopProxy,AdvisedSupport都是作为构造函数参数传入的,里面存储了具体的Advisor。
 
代理对象实例化----通过getProxy方法获取<bean>对应的代理
其实代码已经分析到了JdkDynamicAopProxy和Cglib2AopProxy,剩下的就没什么好讲的了,无非就是看对这两种方式生成代理的熟悉程度而已。
Cglib2AopProxy生成代理的代码就不看了,对Cglib不熟悉的朋友可以看Cglib及其基本使用一文。
JdkDynamicAopProxy生成代理的方式稍微看一下:
1 public Object getProxy(ClassLoader classLoader) {
2 if (logger.isDebugEnabled()) {
3 logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
4 }
5 Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised);
6 findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
7 return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
8 }
这边解释一下第5行和第6行的代码,第5行代码的作用是拿到所有要代理的接口,第6行代码的作用是尝试寻找这些接口方法里面有没有equals方法和hashCode方法,同时都有的话打个标记,寻找结束,equals方法和hashCode方法有特殊处理。
最终通过第7行的Proxy.newProxyInstance方法获取接口/类对应的代理对象,Proxy是JDK原生支持的生成代理的方式。
 
代理方法调用原理
前面已经详细分析了为接口/类生成代理的原理,生成代理之后就要调用方法了,这里看一下使用JdkDynamicAopProxy调用方法的原理。
由于JdkDynamicAopProxy本身实现了InvocationHandler接口,因此具体代理前后处理的逻辑在invoke方法中:
1 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
2 MethodInvocation invocation;
3 Object oldProxy = null;
4 boolean setProxyContext = false;
5
6 TargetSource targetSource = this.advised.targetSource;
7 Class targetClass = null;
8 Object target = null;
9
10 try {
11 if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
12 // The target does not implement the equals(Object) method itself.13 return equals(args[0]);
14 }
15 if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
16 // The target does not implement the hashCode() method itself.17 return hashCode();
18 }
19 if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
20 method.getDeclaringClass().isAssignableFrom(Advised.class)) {
21 // Service invocations on ProxyConfig with the proxy config...22 return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
23 }
24
25 Object retVal;
26
27 if (this.advised.exposeProxy) {
28 // Make invocation available if necessary.29 oldProxy = AopContext.setCurrentProxy(proxy);
30 setProxyContext = true;
31 }
32
33 // May be null. Get as late as possible to minimize the time we "own" the target,
34 // in case it comes from a pool.35 target = targetSource.getTarget();
36 if (target != null) {
37 targetClass = target.getClass();
38 }
39
40 // Get the interception chain for this method.
41 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
42
43 // Check whether we have any advice. If we don't, we can fallback on direct
44 // reflective invocation of the target, and avoid creating a MethodInvocation.
45 if (chain.isEmpty()) {
46 // We can skip creating a MethodInvocation: just invoke the target directly
47 // Note that the final invoker must be an InvokerInterceptor so we know it does
48 // nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
49 retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args);
50 }
51 else {
52 // We need to create a method invocation...
53 invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
54 // Proceed to the joinpoint through the interceptor chain.
55 retVal = invocation.proceed();
56 }
57
58 // Massage return value if necessary.
59 if (retVal != null && retVal == target && method.getReturnType().isInstance(proxy) &&
60 !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
61 // Special case: it returned "this" and the return type of the method
62 // is type-compatible. Note that we can't help if the target sets
63 // a reference to itself in another returned object.64 retVal = proxy;
65 }
66 return retVal;
67 }
68 finally {
69 if (target != null && !targetSource.isStatic()) {
70 // Must have come from TargetSource.71 targetSource.releaseTarget(target);
72 }
73 if (setProxyContext) {
74 // Restore old proxy.75 AopContext.setCurrentProxy(oldProxy);
76 }
77 }
78 }
第11行~第18行的代码,表示equals方法与hashCode方法即使满足expression规则,也不会为之产生代理内容,调用的是JdkDynamicAopProxy的equals方法与hashCode方法。至于这两个方法是什么作用,可以自己查看一下源代码。
第19行~第23行的代码,表示方法所属的Class是一个接口并且方法所属的Class是AdvisedSupport的父类或者父接口,直接通过反射调用该方法。
第27行~第30行的代码,是用于判断是否将代理暴露出去的,由<aop:config>标签中的expose-proxy="true/false"配置。
第41行的代码,获取AdvisedSupport中的所有拦截器和动态拦截器列表,用于拦截方法,具体到我们的实际代码,列表中有三个Object,分别是:
第45行~第50行的代码,如果拦截器列表为空,很正常,因为某个类/接口下的某个方法可能不满足expression的匹配规则,因此此时通过反射直接调用该方法。
第51行~第56行的代码,如果拦截器列表不为空,按照注释的意思,需要一个ReflectiveMethodInvocation,并通过proceed方法对原方法进行拦截,proceed方法感兴趣的朋友可以去看一下,里面使用到了递归的思想对chain中的Object进行了层层的调用。
 
================================================================================== 

我不能保证写的每个地方都是对的,但是至少能保证不复制、不黏贴,保证每一句话、每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后,希望都能看到自己对于技术、对于生活的态度。

我相信乔布斯说的,只有那些疯狂到认为自己可以改变世界的人才能真正地改变世界。面对压力,我可以挑灯夜战、不眠不休;面对困难,我愿意迎难而上、永不退缩。

其实我想说的是,我只是一个程序员,这就是我现在纯粹人生的全部。

==================================================================================



一、对比分析图
稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。
二、冒泡排序
##
#概述
  冒泡排序通过重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来,直到没有再需要交换的元素为止(对n个项目需要O(n^2)的比较次数)。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
#实现步骤
  
  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 
  冒泡排序为一列数字进行排序的过程 
#实现性能
 
O(n^2)
O(n) 
O(n^2)
总共O(n),需要辅助空间O(1)
#Java实现
public static void main(String[] args) {
int[] number = {95,45,15,78,84,51,24,12};
bubble_sort(number);
for(int i = 0; i < number.length; i++) {
System.out.print(number[i] + " ");
}
}
public static void bubble_sort(int[] arr) {
int temp, len = arr.length;
for (int i = 0; i < len - 1; i++)
for (int j = 0; j < len - 1 - i; j++)
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
三、选择排序
##
#选择排序
  常用的选择排序方法有简单选择排序和堆排序,这里只说简单选择排序,堆排序后面再说。
#简单选择排序
  设所排序序列的记录个数为n,i 取 1,2,…,n-1 。   从所有n-i+1个记录(Ri,Ri+1,…,Rn)中找出排序码最小(或最大)的记录,与第i个记录交换。执行n-1趟 后就完成了记录序列的排序。
以排序数组{3,2,1,4,6,5}为例
#简单选择排序性能
  在简单选择排序过程中,所需移动记录的次数比较少。最好情况下,即待排序记录初始状态就已经是正序排列了,则不需要移动记录。    最坏情况下,即待排序记录初始状态是按第一条记录最大,之后的记录从小到大顺序排列,则需要移动记录的次数最多为3(n-1)。
  简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排列情况无关。   当i=1时,需进行n-1次比较;当i=2时,需进行n-2次比较;依次类推,共需要进行的比较次数是(n-1)+(n-2)+…+2+1=n(n-1)/2,即进行比较操作的时间复杂度为O(n^2),进行移动操作的时间复杂度为O(n)。 
  简单选择排序是不稳定排序。
#简单选择排序Java实现
public static void main(String[] args) {
int[] number = {3,1,2,8,4,5,24,12};
SimpleSort(number);
for(int i = 0; i < number.length; i++) {
System.out.print(number[i] + " ");
}
}
public static void SimpleSort(int[] arr) {
int length=arr.length;
int temp;
for(int i=0;i<length-1;i++){
int min=i;
for(int j=i+1;j<length;j++){ //寻找最小的数
if(arr[j]<arr[min]){
min =j;
}
}
if(min!=i){
temp = arr[min];
arr[min]=arr[i];
arr[i]=temp;
}
}
}
四、希尔排序
##
#概述
  希尔排序法(缩小增量法) 属于插入类排序,是将整个无序列分割成若干小的子序列分别进行插入排序的方法。
  把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
#实现过程
  先取一个正整数d1小于n,把所有序号相隔d1的数组元素放一组,组内进行直接插入排序;然后取d2小于d1,重复上述分组和排序操作;直至di=1,即所有记录放进一个组中排序为止。
  例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:
13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10
然后我们对每列进行排序:
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45
  将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].这时10已经移至正确位置了,然后再以3为步长进行排序:
10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45
排序之后变为:
10 14 13 25 23 33 27 25 59 39 65 73 45 94 82 94
最后以1步长进行排序(此时就是简单的插入排序了)。
#实现效率
  希尔排序是一个不稳定的排序,其时间复杂度受**步长(增量)**的影响。
  空间复杂度: O(1)
  时间复杂度: 平均 O(n^1.3)          最好 O(n)          最坏 O(n^2)
#Java实现
  
public static void shellSort(int[] a) {
int gap = 1, i, j, len = a.length;
int temp;//插入排序交换值的暂存
//确定初始步长
while (gap < len / 3){
gap = gap * 3 + 1;
}
for (; gap > 0; gap /= 3){//循环遍历步长,最后必为1
for (i = gap; i < len; i++) {//每一列依次向前做插入排序
temp = a[i];
//每一列中在a[i]上面且比a[i]大的元素依次向下移动
for (j = i - gap; j >= 0 && a[j] > temp; j -= gap){
a[j + gap] = a[j];
}
//a[i]填补空白,完成一列中的依次插入排序
a[j + gap] = temp;
}
}
}
五、归并排序
##
#1.概述
  归并排序,是创建在归并操作上的一种有效的排序算法该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
  即先使每个子序列有序,再将两个已经排序的序列合并成一个序列的操作。若将两个有序表合并成一个有序表,称为二路归并。
例如:
设有数列{6,202,100,301,38,8,1} 初始状态:6,202,100,301,38,8,1 第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3; 第二次归并后:{6,100,202,301},{1,8,38},比较次数:4; 第三次归并后:{1,6,8,38,100,202,301},比较次数:4; 总的比较次数为:3+4+4=11,; 逆序数为14;
      归并排序示意图
#2.效率
  归并排序速度仅次于快速排序,为稳定排序算法(即相等的元素的顺序不会改变),一般用于对总体无序,但是各子项相对有序的数列. 
  时间复杂度为O(nlogn)        空间复杂度为 O(n) 
归并排序比较占用内存,但却是一种效率高且稳定的算法。
#3.迭代实现
   ##3.1实现原理
①申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
②设定两个指针,最初位置分别为两个已经排序序列的起始位置
③比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
④重复步骤③直到某一指针到达序列尾
⑤将另一序列剩下的所有元素直接复制到合并序列尾
##3.2Java代码
public static void main(String[] args) {
int [] arr ={6,5,3,1,8,7,2,4};
merge_sort(arr);
for(int i : arr){
System.out.println(i);
}
}
public static void merge_sort(int[] arr) {
int len = arr.length;
//用于合并的临时数组
int[] result = new int[len];
int block, start;
//两两合并后块大小变大两倍 (注意最后一次block等于len)
for(block = 1; block <=len ; block *= 2) {
//把整个数组分成很多个块,每次合并处理两个块
for(start = 0; start <len; start += 2 * block) {
int low = start;
int mid = (start + block) < len ? (start + block) : len;
int high = (start + 2 * block) < len ? (start + 2 * block) : len;
//两个块的起始下标及结束下标
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
//开始对两个block进行归并排序
while (start1 < end1 && start2 < end2) {
result[low++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
}
while(start1 < end1) {
result[low++] = arr[start1++];
}
while(start2 < end2) {
result[low++] = arr[start2++];
}
}
//每次归并后把结果result存入arr中,以便进行下次归并
int[] temp = arr;
arr = result;
result = temp;
}
}
#4.递归实现
   ##4.1实现原理
假设序列共有n个元素
①将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素。
②将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素
③重复步骤②,直到所有元素排序完毕
##4.2Java代码
public static void main(String[] args) {
int [] arr ={6,5,3,1,8,7,2,4};
int len = arr.length;
int[] reg = new int[len];
merge_sort_recursive(arr,reg,0,len-1);
for(int i : arr){
System.out.println(i);
}
}
static void merge_sort_recursive(int[] arr, int[] reg, int start, int end) {
if (start >= end)
return;
int len = end - start, mid = (len >> 1) + start;
int start1 = start, end1 = mid;
int start2 = mid + 1, end2 = end;
//递归到子序列只有一个数的时候,开始逐个返回
merge_sort_recursive(arr, reg, start1, end1);
merge_sort_recursive(arr, reg, start2, end2);
//-------合并操作,必须在递归之后(子序列有序的基础上)----
int k = start;
while (start1 <= end1 && start2 <= end2)
reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
while (start1 <= end1)
reg[k++] = arr[start1++];
while (start2 <= end2)
reg[k++] = arr[start2++];
//借用reg数组做合并,然后把数据存回arr中
for (k = start; k <= end; k++)
arr[k] = reg[k];
}
六、快速排序
##
#基本思想
  快速排序(Quicksort)是对冒泡排序的一种改进,又称划分交换排序(partition-exchange sort。
  快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
步骤为:
①.从数列中挑出一个元素,称为"基准"(pivot)
②.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
③.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
 
使用快速排序法对一列数字进行排序的过程
#排序效率
  在平均状况下,排序n个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n)算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
最差时间复杂度 Ο(n^2) 
最优时间复杂度 Ο(n log n) 
平均时间复杂度Ο(n log n) 
最差空间复杂度 根据实现的方式不同而不同
#Java实现
public static void main(String[] args) {
int [] arr = {8,1,0,4,6,2,7,9,5,3};
quickSort(arr,0,arr.length-1);
for(int i :arr){
System.out.println(i);
}
}
public static void quickSort(int[]arr,int low,int high){
if (low < high) {
int middle = getMiddle(arr, low, high);
quickSort(arr, low, middle - 1);
quickSort(arr, middle + 1, high);
}
}
public static int getMiddle(int[] list, int low, int high) {
int tmp = list[low];
while (low < high) {
while (low < high && list[high] >= tmp) {
high--;
}
list[low] = list[high];
while (low < high && list[low] <= tmp) {
low++;
}
list[high] = list[low];
}
list[low] = tmp;
return low;
}
运行结果:
分析:
取8为中值,红色箭头表示low,绿色箭头表示high
①从high开始向前扫描到第一个比8小的值与8交换。
②从low向后扫描第一比8大的值与8交换。
③重复①②过程只到,high=low完成一次快速排序,然后递归子序列。
七、堆排序
##
#浅析堆
  堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值。
  由于堆中每次都只能删除第0个数据,通过 取出第0个数据再执行堆的删除操作、重建堆(实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。),然后再取,如此重复实现排序。
堆的操作:
 
在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义以下几种操作:
堆的存储:
通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:
#Java代码实现

public class HeapSort {
private static int[] sort = new int[]{1,0,10,20,3,5,6,4,9,8,12,17,34,11};
public static void main(String[] args) {
buildMaxHeapify(sort);
heapSort(sort);
print(sort);
}

private static void buildMaxHeapify(int[] data){
//没有子节点的才需要创建最大堆,从最后一个的父节点开始
int startIndex = getParentIndex(data.length - 1);
//从尾端开始创建最大堆,每次都是正确的堆
for (int i = startIndex; i >= 0; i--) {
maxHeapify(data, data.length, i);
}
}
/**
* 创建最大堆
* @param data
* @param heapSize需要创建最大堆的大小,一般在sort的时候用到,因为最多值放在末尾,末尾就不再归入最大堆了
* @param index当前需要创建最大堆的位置
*/
private static void maxHeapify(int[] data, int heapSize, int index){
// 当前点与左右子节点比较
int left = getChildLeftIndex(index);
int right = getChildRightIndex(index);
int largest = index;
if (left < heapSize && data[index] < data[left]) {
largest = left;
}
if (right < heapSize && data[largest] < data[right]) {
largest = right;
}
//得到最大值后可能需要交换,如果交换了,其子节点可能就不是最大堆了,需要重新调整
if (largest != index) {
int temp = data[index];
data[index] = data[largest];
data[largest] = temp;
maxHeapify(data, heapSize, largest);
}
}
/**
* 排序,最大值放在末尾,data虽然是最大堆,在排序后就成了递增的
* @param data
*/
private static void heapSort(int[] data) {
//末尾与头交换,交换后调整最大堆
for (int i = data.length - 1; i > 0; i--) {
int temp = data[0];
data[0] = data[i];
data[i] = temp;
maxHeapify(data, i, 0);
}
}
/**
* 父节点位置
* @param current
* @return
*/
private static int getParentIndex(int current){
return (current - 1) >> 1;
}
/**
* 左子节点position注意括号,加法优先级更高
* @param current
* @return
*/
private static int getChildLeftIndex(int current){
return (current << 1) + 1;
}
/**
* 右子节点position
* @param current
* @return
*/
private static int getChildRightIndex(int current){
return (current << 1) + 2;
}
private static void print(int[] data){
for (int i = 0; i < data.length; i++) {
System.out.print(data[i] + " |");
}
}
}

八、桶排序
##
#1.概念
桶排序(Bucket sort)或所谓的箱排序,是一个排序算法。
  假设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]....B[M]中的全部内容即是一个有序序列。
桶排序的步骤:
①设置一个定量的数组当作空桶子。
②寻访序列,并且把项目一个一个放到对应的桶子去。
③对每个不是空的桶子进行排序。
④从不是空的桶子里把项目再放回原来的序列中。
#2.性能
数据结构 数组  最差时间复杂度   O(n^2) 平均时间复杂度  O(n+k) 最差空间复杂度 O(n*k)
  平均情况下桶排序以线性时间运行,桶排序是稳定的,排序非常快,但是同时也非常耗空间,基本上是最耗空间的一种排序算法。
  对N个关键字进行桶排序的时间复杂度分为两个部分:
①循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。
②利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。
  很显然,第②部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:  
① 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。     ②尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。
#3.java实现 
  对0~1之间的一组浮点数进行升序排序:
BucketSort.java
public class BucketSort {
/**
* 对arr进行桶排序,排序结果仍放在arr中
*/
public static void bucketSort(double arr[]){
//-------------------------------------------------分桶-----------------------------------------------
int n = arr.length;
//存放桶的链表
ArrayList bucketList[] = new ArrayList [n];
//每个桶是一个list,存放此桶的元素
for(int i =0;i<n;i++){
//下取等
int temp = (int) Math.floor(n*arr[i]);
//若不存在该桶,就新建一个桶并加入到桶链表中
if(null==bucketList[temp])
bucketList[temp] = new ArrayList();
//把当前元素加入到对应桶中
bucketList[temp].add(arr[i]);
}
//-------------------------------------------------桶内排序-----------------------------------------------
//对每个桶中的数进行插入排序
for(int i = 0;i<n;i++){
if(null!=bucketList[i])
insert(bucketList[i]);
}
//-------------------------------------------------合并桶内数据-----------------------------------------------
//把各个桶的排序结果合并
int count = 0;
for(int i = 0;i<n;i++){
if(null!=bucketList[i]){
Iterator iter = bucketList[i].iterator();
while(iter.hasNext()){
Double d = (Double)iter.next();
arr[count] = d;
count++;
}
}
}
}
/**
* 用插入排序对每个桶进行排序
* 从小到大排序
*/
public static void insert(ArrayList list){
if(list.size()>1){
for(int i =1;i<list.size();i++){
if((Double)list.get(i)<(Double)list.get(i-1)){
double temp = (Double) list.get(i);
int j = i-1;
for(;j>=0&&((Double)list.get(j)>(Double)list.get(j+1));j--)
list.set(j+1, list.get(j)); //后移
list.set(j+1, temp);
}
}
}
}
}
测试代码:
public static void main(String[] args) {
double arr [] ={0.21,0.23,0.76,0.12,0.89};
BucketSort.bucketSort(arr);
for(double a:arr){
System.out.println(a);
}
}
输出结果:
   
九、基数排序
##
#原理
  基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
  将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
#效率
  基数排序的时间复杂度是O(k·n),其中n是排序元素个数,k是数字位数。注意这不是说这个时间复杂度一定优于O(n·log(n)),k的大小取决于数字位的选择和待排序数据所属数据类型的全集的大小;k决定了进行多少轮处理,而n是每轮处理的操作数目。
  基数排序基本操作的代价较小,k一般不大于logn,所以基数排序一般要快过基于比较的排序,比如快速排序。
  最差空间复杂度是O(k·n)
#Java实现
   
  现在有数组:278,109,63,930,589,184,505,269,8,83 。根据各位数将数组划分为10个链表(当然其中的某些链表可能不含有元素)    第一次分配
0:930 1: 2: 3:63,83 4:184 5:505 6: 7: 8:278,8 9:109,589,269
第一次收集后的数组
930,63,83,184,505,278,8,109,589,269
第二次分配
0:505,8,109 1: 2: 3:930 4: 5: 6:63,269 7:278 8:83,184,589 9:
第二次收集后的数组
505,8,109,930,63,269,278,83,184,589
第三次分配:
0:8,63,83 1:109,184 2:278,269 3: 4: 5:505,589 6: 7: 8: 9:930
最后得到序列:
8,63,83,109,184,269,278,505,589,930
基数排序其实是利用多关键字先达到局部有序,再调整达到全局有序。
代码实现:
public class Test {
public static void main(String[] args) {

int[] array = {278,109,63,930,589,184,505,269,8,83};
radixSort(array);
for(double a : array){
System.out.println(a);
}
}
public static void radixSort(int[] array){
//------------------------------------------确定排序的趟数----------------------------------
int max=array[0];
for(int i=1;i<array.length;i++){
if(array[i]>max){
max=array[i];
}
}
int time=0;
while(max>0){
max/=10;
time++;
}
//----------------------------------------初始化10个链表用户分配时暂存-------------------------------
List<List<Integer>> list=new ArrayList<List<Integer>>();
for(int i=0;i<10;i++){
List<Integer> item=new ArrayList<Integer>();
list.add(item);
}
//-----------------------------------------进行time次分配和收集-------------------------------------
for(int i=0;i<time;i++){
//分配元素;
for(int j=0;j<array.length;j++){
int index = array[j]%(int)Math.pow(10, i+1)/(int)Math.pow(10, i);
list.get(index).add(array[j]);
}
//收集元素;
int count=0;
for(int k=0;k<10;k++){
if(list.get(k).size()>0){
for(int a : list.get(k)){
array[count]=a;
count++;
}
//清除数据,以便下次收集
list.get(k).clear();
}
}
}
}
}
运行结果:
十、插入排序
##
#概述
  将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,是稳定的排序方法。
  插入排序又分为 直接插入排序 和 折半插入排序。
#直接插入排序
  把待排序的纪录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的纪录插入完为止,得到一个新的有序序列。
##Java实现
  
public static void insertSort(int a[]){
int j; //当前要插入值的位置
int preJ; //依次指向j前的位置
int key; //后移时来暂存要插入的值
//从数组的第二个位置开始遍历值
for(j=1;j<a.length;j++){
key=a[j];
preJ=j-1;
//a[preJ]比当前值大时,a[preJ]后移一位
while(preJ>=0 && a[preJ]>key){
a[preJ+1]=a[preJ]; //将a[preJ]值后移
//这里注意: a[preJ+1]=a[j]=key,把插入值已经存在了 key中
//等于说, 留出来一个空白位置来实现依次后移(不会造成数据丢失问题)
preJ--; //preJ前移
}
//找到要插入的位置或已遍历完成((preJ=0)
a[preJ+1]=key; //将当前值插入 空白位置
}
}
    备注很清楚,我就不多说了....
##效率分析
  空间复杂度O(1)        平均时间复杂度O(n^2)
  
最差情况:反序,需要移动n*(n-1)/2个元素 ,运行时间为O(n^2)。    最好情况:正序,不需要移动元素,运行时间为O(n).
#折半插入排序 
  直接插入排序中要把插入元素已有序序列元素依次进行比较,效率非常低。       折半插入排序,使用使用折半查找的方式寻找插入点的位置, 可以减少比较的次数,但移动的次数不变, 时间复杂度和空间复杂度和直接插入排序一样,在元素较多的情况下能提高查找性能。
##Java实现
    
private static void binaryInsertSort(int[] a)
{
//从数组的第二个位置开始遍历值
for(int i = 1; i < a.length; i++) {
int key = a[i]; //暂存要插入的值
int pre = 0; //有序序列开始和结尾下标申明
int last = i - 1;
// 折半查找出插入位置 a[pre]
while(pre <= last) {
int mid = (pre + last) / 2;
if(key < a[mid]) {
last = mid - 1;
} else {
pre = mid + 1;
}
}
//a[i]已经取出来存放在key中,把下标从pre + 1到 i-1的元素依次后移
for(int j = i; j >= pre + 1; j--) {
a[j] = a[j - 1];
}
//把值插入空白位置
a[pre] = key;
}
}
直接插入排序是,比较一个后移一个; 折半插入排序是,先找到位置,然后一起移动;
十一、补充
###1. 快排的partition函数
  作用:给定一个数组arr[]和数组中任意一个元素a,重排数组使得a左边都小于它,右边都不小于它。
// A[]为数组,start、end分别为数组第一个元素和最后一个元素的索引
// povitIndex为数组中任意选中的数的索引
static int partition(int A[], int start, int end, int pivotIndex){
int i = start, j = end, pivot = A[pivotIndex];
swap<int>(A[end], A[pivotIndex]);
while(i < j){
while(i < j && A[i] <= pivot) ++i;
while(i < j && A[j] >= pivot) --j;
if(i < j) swap<int>(A[i], A[j]);
}
swap<int>(A[end], A[i]);
return i;
}
###2. 冒泡排序的改进
思路:
①、加一个标志位,当某一趟冒泡排序没有元素交换时,则冒泡结束,元素已经有序,可以有效的减少冒泡次数。
/**
* 引入标志位,默认为true
* 如果前后数据进行了交换,则为true,否则为false。如果没有数据交换,则排序完成。
*/
public static int[] bubbleSort(int[] arr){
boolean flag = true;
int n = arr.length;
while(flag){
flag = false;
for(int j=0;j<n-1;j++){
if(arr[j] >arr[j+1]){
//数据交换
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
//设置标志位
flag = true;
}
}
n--;
}
return arr;
}
②、记录每一次元素交换的位置,当元素交换的位置在第0个元素时,则排序结束。
###3.快排优化
① 快速排序在处理小规模数据时的表现不好,这个时候可以改用插入排序。
②对于一个每个元素都完全相同的一个序列来讲,快速排序也会退化到 O(n^2)。要将这种情况避免到,可以这样做:
  在分区的时候,将序列分为 3 堆,一堆小于中轴元素,一堆等于中轴元素,一堆大于中轴元素,下次递归调用快速排序的时候,只需对小于和大于中轴元素的两堆数据进行排序,中间等于中轴元素的一堆已经放好。

Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行各种刁难。作为一名在互联网技术行业打击过成百上千名【请允许我夸张一下】的资深技术面试官,看过了无数落寞的身影失望的离开,略感愧疚,故献上此文,希望各位读者以后面试势如破竹,永无失败!
Redis有哪些数据结构?
字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。
如果你是Redis中高级用户,还需要加上下面几种数据结构HyperLogLog、Geo、Pub/Sub。
如果你说还玩过Redis Module,像BloomFilter,RedisSearch,Redis-ML,面试官得眼睛就开始发亮了。
使用过Redis分布式锁么,它是什么回事?
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
这时候对方会告诉你说你回答得不错,然后接着问如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?
这时候你要给予惊讶的反馈:唉,是喔,这个锁就永远得不到释放了。紧接着你需要抓一抓自己得脑袋,故作思考片刻,好像接下来的结果是你主动思考出来的,然后回答:我记得set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!对方这时会显露笑容,心里开始默念:摁,这小子还不错。
假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使用keys指令可以扫出指定模式的key列表。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
使用过Redis做异步队列么,你是怎么用的?
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
如果对方追问可不可以不用sleep呢?list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
如果对方追问能不能生产一次消费多次呢?使用pub/sub主题订阅者模式,可以实现1:N的消息队列。
如果对方追问pub/sub有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。
如果对方追问redis如何实现延时队列?我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但是你很克制,然后神态自若的回答道:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
到这里,面试官暗地里已经对你竖起了大拇指。但是他不知道的是此刻你却竖起了中指,在椅子背后。
如果有大量的key需要设置同一时间过期,一般需要注意什么?
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。
Redis如何做持久化的?
bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态。
对方追问那如果突然机器掉电会怎样?取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。
对方追问bgsave的原理是什么?你给出两个词汇就可以了,fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
Pipeline有什么好处,为什么要用pipeline?
可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。
Redis的同步机制了解么?
Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
是否使用过Redis集群,集群的原理是什么?
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

一、Zookeeper简介
1、定义
zookeeper是一个分布式数据一致性解决方案。
2、历史
zookeeper 起源于雅虎研究院的一个研究小组。
3、Zookeeper 特点
顺序一致性:按照客户端发送请求的顺序更新数据。
原子性:更新要么成功,要么失败,不会出现部分更新。
单一性 :无论客户端连接哪个server,都会看到同一个视图。
可靠性:一旦数据更新成功,将一直保持,直到新的更新。
及时性:客户端会在一个确定的时间内得到最新的数据。

二、Zookeeper的使用场景
1、数据发布/订阅
又叫配置中心,即发布者将数据发布到zookeeper集群,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数
据动态更新。
2、命名服务
分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住。被命名的实体通常可以使集群中的机器、提供服
务的地址或远程对象等。比如较为常见的分布式服务框架rpc中的服务地址列表,通过使用命名服务,客户端应用能够根据制定名字来获取服务地址
命名服务提供注册、注销和查看命名等接口。
3、分布式协调/通知
分布式协调/通知服务是分布式系统中不可缺少的一个环节,是将不同分布式组件有机结合起来的关键所在。
对于分布式应用集群而言,引入这样一个协调者,便于将分布式协调职责从应用集群中分离出来,从而大大减少系统间的耦合性,而且能够显著提
高系统的可扩展性。
4、集群管理
所谓集群管理,包括集群监控与集群控制。集群监控侧重对集群运行时状态进行收集;集群控制对集群进行操作和控制。
例如如下需求:
a、希望知道当前集群中究竟有多少机器在工作。
b、对集群中每台机器的运行时状态进行数据收集。
c、对集群中的机器进行上下线操作。
例如分布式日志收集集群。
5、选举master
对于一个集群而言,很多情况需要动态的选举出一个master机器,负责一些特殊的任务。例如数据库集群中选举master机器进行写操作。
6、分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
分为排它锁和共享锁。

三、Zookeeper的实现原理
1、系统模型
( 1 )集群节点角色
1 Leader
 a、事务请求的唯一调度和处理者,保证集群事务处理的顺序;
 b、集群内部各服务器的调度者
2 Follower 
a、处理客户端非事务请求,转发事务请求给Leader服务器;
b、参与事务请求Proposal的投票;
c、参与Leader选举投票
3 Observer
a、处理客户端非事务请求,转发事务请求给Leader服务器;

2 )数据模型
Zookeeper表现为一个分层的文件系统目录树结构(不同于文件系统的是,节点可以有自己的数据,而文件系统中的目录节点只有子节点),每个
数据节点都被称作一个ZNode。
1 持久节点创建后一直存在,直到有删除操作主动清楚该节点。
2 持久顺序节点在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。
3 临时节点,临时节点和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。
4 临时顺序节点,在临时节点上加上顺序特性。

3 )ACL
Zookeeper的ACL,可以从三个维度来理解:一是scheme; 二是user; 三是permission,通常表示为scheme:id:permissions,
下面从这三个方面分别来介绍:
(1)scheme: 缺省支持下面几种scheme:
1 world 它下面只有一个id, 叫anyone, world:anyone代表任何人,zookeeper中对所有人有权限的结点就是属于world:anyone的
2 auth 它不需要id, 只要是通过authentication的user都有权限(zookeeper支持通过kerberos来进行authencation,
也支持username/password形式的authentication)
3 digest 它对应的id为username:BASE64(SHA1(password)),它需要先通过username:password形式的authentication
4 ip 它对应的id为客户机的IP地址,设置的时候可以设置一个ip段,比如ip:192.168.1.0/16, 表示匹配前16个bit的IP段
5 super 在这种scheme情况下,对应的id拥有超级权限,可以做任何事情(cdrwa)

(2)id: id与scheme是紧密相关的,具体的情况在上面介绍scheme的过程都已介绍,这里不再赘述。
(3)permission: zookeeper目前支持下面一些权限:
    1 CREATE 创建权限,可以在在当前node下创建child node
    2 DELETE 删除权限,可以删除当前的node
    3 READ 读权限,可以获取当前node的数据,可以list当前node所有的child nodes
    4 WRITE 写权限,可以向当前node写数据
    5 ADMIN 管理权限,可以设置当前node的permission

(4)版本
发挥着乐观锁的作用
no name description
1 version 当前数据节点数据内容的版本号
2 cversion 当前数据节点子节点的版本号
3 aversion 当前数据节点ACL变更版本号

2、ZAB协议
ZAB协议,全称Zookeeper Atomic
Broadcast,由Paxos算法发展而来。ZAB协议作为Zookeeper集群数据一致性的核心算法,为Zookeeper专门设计的一种支持崩溃恢复的原子广播协议。

Zookeeper使用单一的主进程(即Leader)来接受并处理客户端的所有事务请求;并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Prop
osal的形式广播到所有副本进程(即Follower);之后Leader等待所有Follower反馈,如果有半数Follower进行正确反馈,那么Leader会再次向所有
Follower分发Commit消息,要求将前一Proposal提交,同时Leader自身业务完成对事务的提交。
ZAB协议包括崩溃恢复和消息广播两种模式。当Zookeeper集群处在启动、或是Leader出现网络中断、崩溃退出与重启等异常时,Zookeeper会
进入崩溃恢复模式。之后Zookeeper集群会进行新的Leader选举,当新的Leader选举出来,且集群中过半机器与Leader完成状态同步后,Zookee
per集群会进入消息广播模式。

当处在消息广播模式时。在广播事务Proposal之前 ,Leader会为这个事物分配一个全局单调递增且唯一ID(即ZXID)。由于ZAB协议必须能够保证
一个全局的变更序列被顺序引用,因此必须将每一个事物Proposal按照其ZXID的先后顺序来进行排序处理。
当处在崩溃恢复模式时。ZAB协议需要保证那些已经在Leader上提交的事物最终被所有服务器提交。同时确保丢弃那些只在Leader被提出的事物。
针对这个要求,Leader选举算法需要保证新选举出来的Leader拥有集群中所有机器最大ZXID的事物Proposal。ZXID(64bit)=epoch(32bit)+32bit
单调递增计数。

3、选举leader
选举Leader步骤:
1)自增选举轮次;
2)初始化内部投票;
3)发送内部投票;
4)接收外部投票;
5)判断选举伦次:
    a、如外部投票大于内部投票,则更新内部投票,然后3);
    b、如果外部投票小于内部投票,则忽略;
    c、如外部投票等于内部投票,则6)。
6)投票PK:
    a、先比较ZXID,大者胜出;
    b、如两者ZXID一样,则比较SID,大者胜出。
7)PK结果,如外部投票胜出,则更新内部投票,然后3)
如内部投票胜出,则8)。
8)统计投票:
    a、如过半投票支持内部投票,则9),
    b、否则4)。
9)更新服务器状态:
如内部投票投的是自己,就将自己的状态修改为LEADING;
否则,改为FOLLOWING。

4、Watcher机制
(1)步骤
a、客户端注册Watcher;
b、服务端处理Watcher;
c、客户端回调Watcher

(2)事件类型
KeeperStatus EventType 触发条件说明
SynConnected  None   客户端与服务器端成功建立会话客户端与服务器处于连接状态
NodeCreated    Watcher监听的对应数据节点被创建
NodeDeleted    Watcher监听的对应数据节点被删除
NodeDataChanged    Watcher监听的对应数据节点的数据内容发生变更
NodeChildrenChanged    Watcher监听的对应数据节点的子节点列表发生变更
Disconnected None    客户端与zookeeper服务器断开连接客户端与服务器处于断开状态
Expired None     会话超时客户端会话失效,通常同时也会收到SessionExpiredException
AuthFailed None 通常有两种情况:
a、使用错误的scheme进行权限检查;
b、SASL权限检查失败
通常会收到AuthFailedException异常
3)特点
a、一次性;
b、客户端串行执行
c、轻量

四、总结
ZooKeeper是一个高可用的分布式数据管理与系统协调框架。基于对Paxos算法的改进实现,使该框架保证了分布式环境中数据的强一致性,
也正是基于这样的特性,使得zookeeper能够应用于很多场景。

PPT附件如下:
zookeeper浅析.pptx


  分布式事务是指操作多个数据库之间的事务,spring的org.springframework.transaction.jta.JtaTransactionManager,提供了分布式事务支持。如果使用WAS的JTA支持,把它的属性改为WebSphere对应的TransactionManager。 
       在tomcat下,是没有分布式事务的,不过可以借助于第三方软件jotm(Java Open Transaction Manager )和AtomikosTransactionsEssentials实现,在spring中分布式事务是通过jta(jotm,atomikos)来进行实现。 
1、http://jotm.objectweb.org/
2、http://www.atomikos.com/Main/TransactionsEssentials

一、使用JOTM例子 
(1) Dao及实现 
GenericDao接口:
[java] view plain copy
  1. public interface GenericDao {  
  2.     public int save(String ds, String sql, Object[] obj) throws Exception;    
  3.     public int findRowCount(String ds, String sql);   
  4. }  
GenericDaoImpl 实现:
[java] view plain copy
  1. public class GenericDaoImpl implements GenericDao{  
  2.   
  3.     private  JdbcTemplate jdbcTemplateA;  
  4.     private  JdbcTemplate jdbcTemplateB;  
  5.   
  6.     public void setJdbcTemplateA(JdbcTemplate jdbcTemplate) {  
  7.         this.jdbcTemplateA = jdbcTemplate;  
  8.     }  
  9.   
  10.     public void setJdbcTemplateB(JdbcTemplate jdbcTemplate) {  
  11.         this.jdbcTemplateB = jdbcTemplate;  
  12.     }  
  13.       
  14.     public int save(String ds, String sql, Object[] obj) throws Exception{  
  15.         if(null == ds || "".equals(ds)) return -1;  
  16.         try{  
  17.             if(ds.equals("A")){  
  18.                 return this.jdbcTemplateA.update(sql, obj);  
  19.             }else{  
  20.                 return this.jdbcTemplateB.update(sql, obj);  
  21.             }  
  22.         }catch(Exception e){  
  23.             e.printStackTrace();  
  24.             throw new Exception("执行" + ds + "数据库时失败!");  
  25.         }  
  26.     }  
  27.   
  28.     public int findRowCount(String ds, String sql) {  
  29.         if(null == ds || "".equals(ds)) return -1;  
  30.           
  31.         if(ds.equals("A")){  
  32.             return this.jdbcTemplateA.queryForInt(sql);  
  33.         }else{  
  34.             return this.jdbcTemplateB.queryForInt(sql);  
  35.         }  
  36.     }  
  37. }  

(2) Service及实现 
UserService 接口:
[java] view plain copy
  1. public interface UserService {  
  2.     public void saveUser() throws Exception;  
  3. }  
UserServiceImpl 实现:
[java] view plain copy
  1. public class UserServiceImpl implements UserService{  
  2.   
  3.     private GenericDao genericDao;  
  4.       
  5.     public void setGenericDao(GenericDao genericDao) {  
  6.         this.genericDao = genericDao;  
  7.     }  
  8.   
  9.     public void saveUser() throws Exception {  
  10.         String userName = "user_" + Math.round(Math.random()*10000);  
  11.         System.out.println(userName);  
  12.           
  13.         StringBuilder sql = new StringBuilder();  
  14.         sql.append(" insert into t_user(username, gender) values(?,?); ");  
  15.         Object[] objs = new Object[]{userName,"1"};  
  16.           
  17.         genericDao.save("A", sql.toString(), objs);  
  18.           
  19.         sql.delete(0, sql.length());  
  20.         sql.append(" insert into t_user(name, sex) values(?,?); ");  
  21.         objs = new Object[]{userName,"男的"};//值超出范围  
  22.         genericDao.save("B", sql.toString(), objs);  
  23.     }  
  24. }  

(3) applicationContext-jotm.xml 
[java] view plain copy
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"   
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
  4.     xmlns:context="http://www.springframework.org/schema/context"   
  5.     xmlns:aop="http://www.springframework.org/schema/aop"   
  6.     xmlns:tx="http://www.springframework.org/schema/tx"   
  7.     xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
  8.     http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-2.5.xsd  
  9.     http://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx-2.5.xsd  
  10.     http://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-2.5.xsd">  
  11.   
  12.     <description>springJTA</description>  
  13.   
  14.     <!--指定Spring配置中用到的属性文件-->   
  15.     <bean id="propertyConfig"   
  16.             class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">   
  17.         <property name="locations">   
  18.             <list>   
  19.                 <value>classpath:jdbc.properties</value>   
  20.             </list>   
  21.         </property>   
  22.     </bean>   
  23.       
  24.     <!-- JOTM实例 -->  
  25.     <bean id="jotm" class="org.springframework.transaction.jta.JotmFactoryBean">  
  26.           <property name="defaultTimeout" value="500000"/>  
  27.     </bean>  
  28.   
  29.     <!-- JTA事务管理器 -->  
  30.     <bean id="jtaTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">     
  31.         <property name="userTransaction" ref="jotm" />     
  32.     </bean>  
  33.   
  34.     <!-- 数据源A -->   
  35.     <bean id="dataSourceA" class="org.enhydra.jdbc.pool.StandardXAPoolDataSource" destroy-method="shutdown">   
  36.        <property name="dataSource">   
  37.            <bean class="org.enhydra.jdbc.standard.StandardXADataSource" destroy-method="shutdown">   
  38.                <property name="transactionManager" ref="jotm"/>   
  39.                <property name="driverName" value="${jdbc.driver}"/>   
  40.                <property name="url" value="${jdbc.url}"/>   
  41.            </bean>   
  42.        </property>   
  43.        <property name="user" value="${jdbc.username}"/>   
  44.        <property name="password" value="${jdbc.password}"/>   
  45.     </bean>   
  46.   
  47.     <!-- 数据源B -->   
  48.     <bean id="dataSourceB" class="org.enhydra.jdbc.pool.StandardXAPoolDataSource" destroy-method="shutdown">   
  49.        <property name="dataSource">   
  50.            <bean class="org.enhydra.jdbc.standard.StandardXADataSource" destroy-method="shutdown">   
  51.                <property name="transactionManager" ref="jotm"/>   
  52.                <property name="driverName" value="${jdbc2.driver}"/>   
  53.                <property name="url" value="${jdbc2.url}"/>   
  54.            </bean>   
  55.        </property>   
  56.        <property name="user" value="${jdbc2.username}"/>   
  57.        <property name="password" value="${jdbc2.password}"/>   
  58.     </bean>   
  59.   
  60.     <bean id = "jdbcTemplateA"   
  61.          class = "org.springframework.jdbc.core.JdbcTemplate">   
  62.          <property name = "dataSource" ref="dataSourceA"/>   
  63.     </bean>  
  64.       
  65.     <bean id = "jdbcTemplateB"   
  66.          class = "org.springframework.jdbc.core.JdbcTemplate">   
  67.          <property name = "dataSource" ref="dataSourceB"/>   
  68.     </bean>      
  69.   
  70.     <!-- 事务切面配置 -->   
  71.     <aop:config>   
  72.         <aop:pointcut id="pointCut"  
  73.                 expression="execution(* com.logcd.service..*.*(..))"/><!-- 包及其子包下的所有方法 -->  
  74.         <aop:advisor pointcut-ref="pointCut" advice-ref="txAdvice"/>   
  75.           
  76.         <aop:advisor pointcut="execution(* *..common.service..*.*(..))" advice-ref="txAdvice"/>  
  77.     </aop:config>   
  78.   
  79.     <!-- 通知配置 -->   
  80.     <tx:advice id="txAdvice" transaction-manager="jtaTransactionManager">   
  81.        <tx:attributes>   
  82.           <tx:method name="delete*" rollback-for="Exception"/>   
  83.           <tx:method name="save*" rollback-for="Exception"/>   
  84.           <tx:method name="update*" rollback-for="Exception"/>   
  85.           <tx:method name="find*" read-only="true" rollback-for="Exception"/>   
  86.        </tx:attributes>   
  87.     </tx:advice>   
  88.   
  89.     <bean id="genericDao"  class="com.logcd.dao.impl.GenericDaoImpl" autowire="byName"> </bean>  
  90.     <bean id="userService"  class="com.logcd.service.impl.UserServiceImpl" autowire="byName"> </bean>  
  91. </beans>  

(4) 测试 
[java] view plain copy
  1. public class TestUserService{  
  2.   
  3.     private static UserService userService;  
  4.       
  5.     @BeforeClass  
  6.     public static void init(){  
  7.         ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext-jotm.xml");  
  8.         userService = (UserService)app.getBean("userService");  
  9.     }  
  10.       
  11.     @Test  
  12.     public void save(){  
  13.         System.out.println("begin...");  
  14.         try{  
  15.             userService.saveUser();  
  16.         }catch(Exception e){  
  17.             System.out.println(e.getMessage());  
  18.         }  
  19.         System.out.println("finish...");  
  20.     }  
  21. }  


二、关于使用atomikos实现 
(1) 数据源配置 
[java] view plain copy
  1. <bean id="dataSourceA" class="com.atomikos.jdbc.SimpleDataSourceBean" init-method="init" destroy-method="close">  
  2.     <property name="uniqueResourceName">  
  3.         <value>${datasource.uniqueResourceName}</value>  
  4.     </property>  
  5.     <property name="xaDataSourceClassName">   
  6.         <value>${database.driver_class}</value>   
  7.     </property>   
  8.     <property name="xaDataSourceProperties">  
  9.         <value>URL=${database.url};user=${database.username};password=${database.password}</value>   
  10.     </property>   
  11.     <property name="exclusiveConnectionMode">   
  12.         <value>${connection.exclusive.mode}</value>   
  13.     </property>  
  14.     <property name="connectionPoolSize">   
  15.         <value>${connection.pool.size}</value>  
  16.     </property>  
  17.     <property name="connectionTimeout">  
  18.         <value>${connection.timeout}</value>  
  19.     </property>  
  20.     <property name="validatingQuery">   
  21.         <value>SELECT 1</value>   
  22.     </property>   
  23. </bean>  

(2)、事务配置 
[java] view plain copy
  1. <bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager"   
  2.     init-method="init" destroy-method="close">   
  3.     <property name="forceShutdown" value="true"/>   
  4. </bean>   
  5.   
  6. <bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp">   
  7.     <property name="transactionTimeout" value="${transaction.timeout}"/>   
  8. </bean>  
  9.   
  10. <!-- JTA事务管理器 -->   
  11. <bean id="springTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">   
  12.     <property name="transactionManager" ref="atomikosTransactionManager"/>   
  13.     <property name="userTransaction" ref="atomikosUserTransaction"/>   
  14. </bean>  
  15.   
  16. <!-- 事务切面配置 -->   
  17. <aop:config>   
  18.     <aop:pointcut id="serviceOperation"  expression="execution(* *..service*..*(..))"/>   
  19.     <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>   
  20. </aop:config>  
  21.   
  22. <!-- 通知配置 -->  
  23. <tx:advice id="txAdvice" transaction-manager="springTransactionManager">   
  24.     <tx:attributes>  
  25.         <tx:method name="*" rollback-for="Exception"/>   
  26.     </tx:attributes>   
  27. </tx:advice>   

有关JTA
JTA全称为Java Transaction API,顾名思义JTA定义了一组统一的事务编程的接口,这些接口如下:
 
XAResource 
XAResource接口是对实现了X/Open CAE规范的资源管理器 (Resource Manager,数据库就是典型的资源管理器) 的抽象,它由资源适配器 (Resource Apdater) 提供实现。XAResource是支持事务控制的核心。

Transaction
Transaction接口是一个事务实例的抽象,通过它可以控制事务内多个资源的提交或者回滚。二阶段提交过程也是由Transaction接口的实现者来完成的。

TransactionManager
托管模式 (managed mode) 下,TransactionManager接口是被应用服务器调用,以控制事务的边界的。
 
UserTransaction
非托管模式 (non-managed mode) 下,应用程序可以通过UserTransaction接口控制事务的边界
 
托管模式下的事务提交场景
注意:在上图中3和5的步骤之间省略了应用程序对资源的操作 (如CRUD)。另外,应用服务器什么时机 enlistResource,又是什么时候delistResource呢?这在后文中会解释。
有关JCA
下图为JCA的架构图
中间涉及元素说明如下:
1)Enterprise Information System
简称EIS,在JTA中它又被称为资源管理器。典型的EIS有数据库,事务处理系统(Transaction Processing System),ERP系统。

2)Resource Adapter
资源适配器(Resource Adaper)是JCA的关键。要想把不同的EIS整合(或者连接)到J2EE运行环境中,就必须为每个EIS提供资源适配器,它会将将EIS适配为一个具备统一编程接口的资源 (Resource) 。这个统一编程接口就是上图中的System Contracts和Client API。下面的UML类图将完美诠释资源适配器。
3)Application Server
应用服务器 (Application Server) 通过System Contracts来管理对EIS的安全、事务、连接等。典型的应用服务器有JBoss、JOnAS、Geronimo、GlassFish等。
 
4)Application Component 
应用组件 (Application Component) ,它封装了应用业务逻辑,像对资源的访问和修改。典型的应用组件就是EJB。
 
更多细节请参见:
Sun Microsystems Inc.J2EE Connector Architecture 1.5 
参考推荐:
Spring分布式事务实现
JTA与JCA分布式事务
理解 JCA 事务(IBM)
Jencks实现Hibernate与Jackrabbit的分布式事务
原文地址:http://blog.csdn.net/ithomer/article/details/10859235

背景

大多数系统都是从一个单一系统开始起步的,随着公司业务的快速发展,这个单一系统变得越来越庞大,带来几个问题:
  1. 随着访问量的不断攀升,纯粹通过提升机器的性能来已经不能解决问题,系统无法进行有效的水平扩展
  2. 维护这个单一系统,变得越来越复杂
  3. 同时,随着业务场景的不同以及大研发的招兵买马带来了不同技术背景的工程师,在原有达达Python技术栈的基础上,引入了Java技术栈。
如何来解决这些问题?业务服务化是个有效的手段来解决大规模系统的性能瓶颈和复杂性。通过系统拆分将原有的单一庞大系统拆分成小系统,它带来了如下好处:
  1. 原来系统的压力得到很好的分流,有效地解决了原先系统的瓶颈,同时带来了更好的扩展性
  2. 独立的代码库,更少的业务逻辑,系统的维护性得到极大的增强
同时,也带来了一系列问题:
  • 随着系统服务的越来越多,如何来管理这些服务?
  • 如何分发请求到提供同一服务的多台主机上(负载均衡如何来做)
  • 如果提供服务的Endpoint发生变化,如何将这些信息通知服务的调用方?

最初的解决方案

Linkedin的创始人里德霍夫曼曾经说过:
成立一家初创公司就像把自己从悬崖上扔下来,在降落过程中去组装一架飞机。
这对于初创公司达达也是一样的,业务在以火箭般的速度发展着。技术在业务发展中作用就是保障业务的稳定运行,快速地“组装一架飞机”。所以,在业务服务化的早期,我们采用了Nginx+本地hosts文件的方式进行内部服务的注册与发现,架构图如下:
各系统组件角色如下:
  1. 服务消费者通过本地hosts中的服务提供者域名与Nginx的IP绑定信息来调用服务
  2. Nginx用来对服务提供者提供的服务进行存活检查和负载均衡
  3. 服务提供者提供服务给服务消费者访问,并通过Nginx来进行请求分发
这在内部系统比较少,访问量比较小的情况下,解决了服务的注册,发现与负载均衡等问题。但是,随着内部服务越来愈多,访问量越来越大的情况下,该架构的隐患逐渐暴露出来:

如何来解决

在谈如何来解决之前,现梳理一下服务注册与发现的目标:

备选方案一: DNS

DNS作为服务注册发现的一种方案,它比较简单。只要在DNS服务上,配置一个DNS名称与IP对应关系即可。定位一个服务只需要连接到DNS服务器上,随机返回一个IP地址即可。由于存在DNS缓存,所以DNS服务器本身不会成为一个瓶颈。
这种基于Pull的方式不能及时获取服务的状态的更新(例如:服务的IP更新等)。如果服务的提供者出现故障,由于DNS缓存的存在,服务的调用方会仍然将请求转发给出现故障的服务提供方;反之亦然。

备选方案二:Dubbo

Dubbo是阿里巴巴推出的分布式服务框架,致力于解决服务的注册与发现,编排,治理。它的优点如下:
  1. 功能全面,易于扩展
  2. 支持各种序列化协议(JSON,Hession,java序列化等)
  3. 支持各种RPC协议(HTTP,Java RMI,Dubbo自身的RPC协议等)
  4. 支持多种负载均衡算法
  5. 其他高级特性:服务编排,服务治理,服务监控等
缺点如下:
  1. 只支持Java,对于Python没有相应的支持
  2. 虽然已经开源,但是没有成熟的社区来运营和维护,未来升级可能是个麻烦
  3. 重量级的解决方案带来新的复杂性

备选方案三:Zookeeper

Zookeeper是什么?按照Apache官网的描述是:
ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services.
参照官网的定义,它能够做:
  1. 作为配置信息的存储的中心服务器
  2. 命名服务
  3. 分布式的协调
  4. Mater选举等
在定义中特别提到了命名服务。在调研之后,Zookeeper作为服务注册与发现的解决方案,它有如下优点:
  1. 它提供的简单API
  2. 已有互联网公司(例如:Pinterest,Airbnb)使用它来进行服务注册与发现
  3. 支持多语言的客户端
  4. 通过Watcher机制实现Push模型,服务注册信息的变更能够及时通知服务消费方
缺点是:
  1. 引入新的Zookeeper组件,带来新的复杂性和运维问题
  2. 需自己通过它提供的API来实现服务注册与发现逻辑(包含Python与Java版本)
我们对上述几个方案的优缺点权衡之后,决定采用了基于Zookeeper实现自己的服务注册与发现。

基于Zookeeper的服务注册与发现架构

在此架构中有三类角色:服务提供者,服务注册中心,服务消费者。
服务提供者
服务提供者作为服务的提供方将自身的服务信息注册到服务注册中心中。服务信息包含:
服务注册中心
服务注册中心主要提供所有服务注册信息的中心存储,同时负责将服务注册信息的更新通知实时的Push给服务消费者(主要是通过Zookeeper的Watcher机制来实现的)。
服务消费者
服务消费者主要职责如下:
  1. 服务消费者在启动时从服务注册中心获取需要的服务注册信息
  2. 将服务注册信息缓存在本地
  3. 监听服务注册信息的变更,如接收到服务注册中心的服务变更通知,则在本地缓存中更新服务的注册信息
  4. 根据本地缓存中的服务注册信息构建服务调用请求,并根据负载均衡策略(随机负载均衡,Round-Robin负载均衡等)来转发请求
  5. 对服务提供方的存活进行检测,如果出现服务不可用的服务提供方,将从本地缓存中剔除
服务消费者只在自己初始化以及服务变更时会依赖服务注册中心,在此阶段的单点故障通过Zookeeper集群来进行保障。在整个服务调用过程中,服务消费者不依赖于任何第三方服务。

实现机制介绍

Zookeeper数据模型介绍

在整个服务注册与发现的设计中,最重要是如何来存储服务的注册信息。
在设计基于Zookeeper的服务注册结构之前,我们先来看一下Zookeeper的数据模型。Zookeeper的数据模型如下图所示:
Zookeeper数据模型结构与Unix文件系统很类似,是一个树状层次结构。每个节点叫做Znode,节点可以拥有子节点,同时允许将少量数据存储在该节点下。客户端可以通过监听节点的数据变更和子节点变更来实时获取Znode的变更(Wather机制)。

服务注册结构

服务注册结构如上图所示。

实现机制

由于目前服务注册通过我们的服务注册中心UI来进行注册,这部分逻辑比较简单,即通过UI界面来构造上述定义的服务注册结构。
下面着重介绍一下我们的服务发现是如何工作的:
在上述类图中,类ServiceDiscovery主要通过Zookeeper的API(Python/Java版本)来获取服务信息,同时对服务注册结构中的每个服务的providers节点增加Watcher,来监控节点变化。获取的服务注册信息保存在变量service_repos中。通过在初始化时设置LoadBalanceStrategy的实现(Round-Robin算法,Radmon算法)来实现服务提供者的负载均衡。主要方法:
  1. init获取Zookeeper的服务注册信息,并缓存在service_repos
  2. get_service_repos方法获取实例变量service_repos
  3. get_service_endpoint根据init构建好的service_repos,以及lb_strategy提供的负载均衡策略返回某个服务的URL地址
  4. update_service_repos通过Zookeeper的Watcher机制来实时更新本地缓存service_repos
  5. heartbeat_monitor是一个心跳检测线程,用来进行服务提供者的健康存活检测,如果出现问题,将该服务提供者从该服务的提供者列表中移除;反之,则加入到服务的提供者列表中
LoadBalanceStrategy定义了根据服务提供者的信息返回对应的服务Host和IP,即决定由那台主机+端口来提供服务。
RoundRobinStrategy和RandomStrategy分别实现了Round-Robin和随机的负载均衡算法

未来展望

目前达达基于Zookeeper的服务注册与发现的架构还处于初期,很多功能还未完善,例如:服务的路由功能,与部署平台的集成,服务的监控等等。
当然基于Zookeeper还能做其它许多事情,例如:实时动态配置系统。目前,我们已经基于Zookeeper实现了实时动态配置系统。
原文地址:https://tech.imdada.cn/2015/12/03/service-registry-and-discovery-with-zk/

一.使用分布式事务



二.使用幂等性设计(TICKET)



三.使用分布式锁

以下是摘自《Understanding SOA with Web Services》(中文版)关于两个概念的解释: 
编制(orchestration)和编排(choreography)是常用于描述“合成Web服务的两种方式”的术语。虽然它们有共同之处,但还是有些区别的。Web服务编制(Web Services Orchestration,WSO)指为业务流程(business processes)而进行Web服务合成,而Web服务编排(Web Services Choreography,WSC)指为业务协作(business collaborations)而进行Web服务合成。 

WSO关注于以一种说明性的(declarative)方式(而不是编程的方式)创建合成服务。WSO定义了组成编制(orchestration)的服务,以及这些服务的执行顺序(比如并行活动、条件分支逻辑等)。因此,可以将编制(orchestration)视为一种简单的流程,这种流程自身也是一个Web服务。WSO流通常包括分支控制点、并行处理选择、人类响应步骤以及各种类型的预定义步骤(例如转换、适配器、电子邮件及Web服务等)。 

WSC关注于定义多方如何在一个更大的业务事务中进行协作。WSC通过“各方描述自己如何与其他Web服务进行公共消息交换”来定义业务交互,而不是像WSO中那样描述一方是如何执行某个具体业务流程的。 
在用WSC来定义业务交互时,需要一个对“业务流程在交互过程中所使用的消息交换协议”的正式描述,对在“有状态的、长期运行的、涉及多方的流程”中的对等的(peer-to-peer)消息交换(同步的或异步的)进行建模。 

WSO与WSC的关键区别在于:WSC是一种对等模型(peer-to-peer model),业务流程中会有很多协作方;而WSO是一种层次化的请求者/提供者模型(hierarchical requester/provider model),WSO仅定义了应调用什么服务以及应该何时调用,没有定义多方如何进行协作。

Dubbo是阿里多年前开源的一套服务治理框架,在众多互联网企业里应用广泛。本文介绍了一些如何监控与管理dubbo服务。使用的工具与《dubbox 的各种管理和监管》大致相同,本文更侧重于命令细节与实践。
首先参考《服务治理框架dubbo上手指南》,实现自己的HelloService服务。在此基础上运行Provider.java启动服务,接下来就可以开始管理工作啦。让我们来下载dubbo的源码并安装:
1
2
3
cd ~/dubbo
mvn clean install -Dmaven.test.skip

telnet

Dubbo支持使用telnet来查看服务状态。下面让我们来试一下:
1
telnet localhost 19880
敲下回车便能看到dubbo>的提示符了。以下命令可以看到服务列表、服务的方法详细信息列表和服务地址列表:
1
2
3
ls
ls -l org.ggg.hello.service.HelloService
ps -l
以下命令可以跟踪服务方法的调用情况:
1
trace org.ggg.hello.service.HelloService
这时会看见光标停止住了。运行Consumer.java来消费服务,就可以看到跟踪结果啦。详细的命令可以参考Telnet命令参考手册。若是服务端没有开启监控,使用telnet命令对查看甚至配置服务来说,是依赖最少最简便的方式。

简易监控中心

先前安装过的dubbo项目里包含了dubbo-monitor-simple的包,解压之:
1
tar zxvf dubbo-simple/dubbo-monitor-simple/target/dubbo-monitor-simple-2.5.4-SNAPSHOT-assembly.tar.gz
配置dubbo.properties为自己的zk服务地址:192.168.33.88:2181
1
2
3
4
# 操作系统是Linux的话
sed -i "s/multicast:\/\/224.5.6.7:1234/zookeeper:\/\/192.168.33.88:2181/" dubbo-monitor-simple-2.5.4-SNAPSHOT/conf/dubbo.properties
# 操作系统是OS X的话(接下来的sed命令以OS X为例)
sed -i "" "s/multicast:\/\/224.5.6.7:1234/zookeeper:\/\/192.168.33.88:2181/" dubbo-monitor-simple-2.5.4-SNAPSHOT/conf/dubbo.properties
简易监控中心以文件的方式记录监控数据。创建基本文件夹并运行start.sh启动简易监控中心:
1
2
mkdir ~/monitor
dubbo-monitor-simple-2.5.4-SNAPSHOT/bin/start.sh
访问http://localhost:8080/便能看到简易监控中心的首页了:
Applications里可以看到我们先前启动的hello-world-appsimple-monitor自己。可见simple-monitor也是一个dubbo服务,把自己注册到dubbo.properties里指定的配置中心里去。为了接收到监控数据,需要在dubbo服务端打开监控。在provider.xml文件里加入下面这行配置,然后重新启动dubbo服务:
1
<dubbo:monitor protocol="registry" />
过一会儿就能看到监控的统计信息和图标了:
而运行ls ~/monitor/也能看到简易监控中心所生成的文件夹chartsstatistics。如果迟迟没有生成这些信息,有一种可能是在多(虚拟)网卡的情况下,simple-monitor绑定到了错误的IP地址去了。万一真是如此,在dubbo.properties里增加一行dubbo.protocol.host的配置即可。
运行stop.sh停止简易监控中心服务:
1
dubbo-monitor-simple-2.5.4-SNAPSHOT/bin/stop.sh

管理控制台

Dubbo为服务治理提供了管理控制台。这是一个webapp,可以很轻松地运行在web容器中。经由源代码安装后便会生成dubbo-admin-2.5.4-SNAPSHOT.war文件,将其部署在tomcat容器中即可。这里用docker启动一个tomcat 7:
1
2
3
4
docker run -d -p 8080:8080 --name=tomcat tomcat:7
docker cp dubbo-admin/target/dubbo-admin-2.5.4-SNAPSHOT.war tomcat:/usr/local/tomcat/webapps/
docker exec tomcat sed -i "s/127.0.0.1/192.168.33.88/" /usr/local/tomcat/webapps/dubbo-admin-2.5.4-SNAPSHOT/WEB-INF/dubbo.properties
docker restart tomcat
文件dubbo.properties里配置了zookeep的地址,所以用sed将其替换为自己的zk服务地址:192.168.33.88。里面还配置了root账户和guest账户的密码。默认即为rootguest。访问http://localhost:8080/dubbo-admin-2.5.4-SNAPSHOT/并输入rootroot,就能看到管理控制台的首页了:
输入hello进行查询,便能看到我们的服务了:
可以在此页面进行各种服务治理操作。更详细的资料,请参考运维手册
运行以下命令删除tomcat容器:
1
docker rm -f tomcat

dubbo-monitor

简易监控中心和管理控制台的页面风格看起来还停留在上个世纪。韩都衣舍提供了一个dubbo-monitor,除了更加现代化的页面,还用数据库代替了简易监控中心写文件的方式。除了mysql,还支持mongo。那我们先来下载代码:
1
2
3
cd ..
cd dubbo-monitor
然后启动一个mysql数据库实例:
1
2
docker run -d --name=mysql -e MYSQL_ROOT_PASSWORD=raycool -v `pwd`/sql/create.sql:/create.sql mysql:5.7.12
docker exec -it mysql mysql -uroot -praycool
创建数据库和表(可能需要等数据库启动一小会儿之后才能连上去):
1
2
3
4
5
CREATE DATABASE dubbokeeper DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
USE dubbokeeper;
SOURCE /create.sql;
SHOW TABLES;
exit
修改配置文件application.properties并打包(目前版本中spring的引用有问题,所以也需要修改pom.xml):
1
2
3
4
MYSQL_IP=`docker exec -it mysql hostname -i | sed 's/.$//'`
sed -i "" -e "s/dubbo.registry.address.*/dubbo.registry.address=zookeeper:\/\/192.168.33.88:2181/" -e "s/default/${MYSQL_IP}/" src/main/resources/application.properties
sed -i "" s/4.1.6.RELEASE/3.2.9.RELEASE/" pom.xml
mvn clean package
启动dubbo-monitor:
1
docker run -d -p 8080:8080 -v `pwd`/target/dubbo-monitor-x.war:/usr/local/tomcat/webapps/dubbo-monitor-x.war --name=tomcat tomcat:7
访问http://localhost:8080/dubbo-monitor-x/并输入adminadmin,就能看到dubbo-monitor的首页了:
可以在dubbo-monitor提供的页面上监控各种数据,但由于笔者是mac配docker版tomcat的方式,监控的dubbo服务只能绑定在docker内部IP上,导致监控数据过不去(com.alibaba.dubbo.remoting.RemotingException: message can not send, because channel is closed)。真正运行的时候还是使用linux+docker(配合net=host)或是mac+原生tomcat的方式吧。运行以下命令删除mysql和tomcat容器:
1
docker rm -f mysql tomcat

DubboKeeper

除了dubbo-monitor以外,github里还有一个社区版的DubboKeeper,功能最为强大。既提供监控功能,又提供服务治理功能。监控数据的持久化除了支持mysql和mongo,还支持lucene。现在下载代码并打包。Dubbokeeper的打包方式略有些不同,需要执行根目录里的install-xxx.sh脚本。这里还是以mysql为例:
1
2
3
4
cd ..
cd dubbokeeper
./install-mysql.sh
首先启动DubboKeeper的UI:
1
2
3
docker run -d -p 8080:8080 -v `pwd`/target/mysql-dubbokeeper-ui/dubbokeeper-ui-1.0.1.war:/usr/local/tomcat/webapps/dubbokeeper-ui-1.0.1.war --name=tomcat tomcat:7
docker exec tomcat sed -i "s/localhost/192.168.33.88/" /usr/local/tomcat/webapps/dubbokeeper-ui-1.0.1/WEB-INF/classes/dubbo.properties
docker restart tomcat
访问http://localhost:8080/dubbokeeper-ui-1.0.1/index.htm#/statistics,就能看到DubboKeeper的首页了:
很显然在这些UI里,DubboKeeper的逼格是最高的。第二个tab就是Admin(管理):
接下来就是Monitor(监控),不过只有启动过监控数据存储端才能使用,现在点击会报后端系统出现异常,请稍后再试的错。接下来启动数据库:
1
2
docker run -d --name=mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=19890314 -v `pwd`/doc/storage/mysql/sql/application.sql:/create.sql mysql:5.7.12
docker exec -it mysql mysql -uroot -p19890314
初始化数据库:
1
2
3
4
5
CREATE DATABASE dubbokeeper;
USE dubbokeeper;
SOURCE /create.sql;
SHOW TABLES;
exit
修改监控数据存储端的配置并启动之:
1
2
3
sed -i "" -e "s/localhost:2181/192.168.33.88:2181/" -e "s/dubbo-monitor/dubbokeeper/" target/mysql-dubbokeeper-server/conf/dubbo-mysql.properties
chmod +x target/mysql-dubbokeeper-server/bin/start-mysql.sh
target/mysql-dubbokeeper-server/bin/start-mysql.sh
现在就可以顺利打开监控页面了:
DubboKeeper甚至还提供了ZooKeeper的窥视版ZooPeeper
最后收尾,Ctrl+C掉监控数据存储端,并删除各个容器:
1
docker rm -f mysql tomcat



Dubbo 是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring 框架无缝集成。
三大主要核心

Dubbo 工作原理

四大组件
(1) 连通性
(2) 健状性
(3) 伸缩性


最近一段时间不论互联网还是传统行业,凡是涉及信息技术范畴的圈子几乎都在讨论微服务架构。近期也看到各大技术社区开始组织一些沙龙和论坛来分享Spring Cloud的相关实施经验,这对于最近正在整理Spring Cloud相关套件内容与实例应用的我而言,还是有不少激励的。
目前,Spring Cloud在国内的知名度并不高,在前阵子的求职过程中,与一些互联网公司的架构师、技术VP或者CTO在交流时,有些甚至还不知道该项目的存在。可能这也与国内阿里巴巴开源服务治理框架Dubbo有一定的关系,除了Dubbo本身较为完善的中文文档之外,不少科技公司的架构师均出自阿里系,所以就目前情况看,短期国内还是Dubbo的天下。
那么第一次实施微服务架构时,我们应该选择哪个基础框架更好呢?
以下内容均为作者个人观点,知识面有限,如有不对,纯属正常,不喜勿喷。

Round 1:背景


Dubbo,是阿里巴巴服务化治理的核心框架,并被广泛应用于阿里巴巴集团的各成员站点。阿里巴巴近几年对开源社区的贡献不论在国内还是国外都是引人注目的,比如:JStorm捐赠给Apache并加入Apache基金会等,为中国互联网人争足了面子,使得阿里巴巴在国人眼里已经从电商升级为一家科技公司了。
Spring Cloud,从命名我们就可以知道,它是Spring Source的产物,Spring社区的强大背书可以说是Java企业界最有影响力的组织了,除了Spring Source之外,还有Pivotal和Netfix是其强大的后盾与技术输出。其中Netflix开源的整套微服务架构套件是Spring Cloud的核心。
小结:如果拿Dubbo与Netflix套件做对比,前者在国内影响力较大,后者在国外影响力较大,我认为在背景上可以打个平手;但是若要与Spring Cloud做对比,由于Spring Source的加入,在背书上,Spring Cloud略胜一筹。不过,英雄不问出处,在背景这一点上,不能作为选择框架的主要因素,当您一筹莫展的时候,可以作为参考依据。

Round 2:社区活跃度


我们选择一个开源框架,社区的活跃度是我们极为关注的一个要点。社区越活跃,解决问题的速度越快,框架也会越来越完善,不然当我们碰到问题,就不得不自己解决。而对于团队来说,也就意味着我们不得不自己去维护框架的源码,这对于团队来说也将会是一个很大的负担。
下面看看这两个项目在github上的更新时间,下面截图自2016年7月30日:
最后更新时间为:2016年5月6日
最后更新时间为:12分钟前
可以看到Dubbo的更新已经是几个月前,并且更新频率很低。而Spring Cloud的更新是12分钟前,仍处于高速迭代的阶段。
小结:在社区活跃度上,Spring Cloud毋庸置疑的优于Dubbo,这对于没有大量精力与财力维护这部分开源内容的团队来说,Spring Cloud会是更优的选择。

Round 3:架构完整度


或许很多人会说Spring Cloud和Dubbo的对比有点不公平,Dubbo只是实现了服务治理,而Spring Cloud下面有17个子项目(可能还会新增)分别覆盖了微服务架构下的方方面面,服务治理只是其中的一个方面,一定程度来说,Dubbo只是Spring Cloud Netflix中的一个子集。但是在选择框架上,方案完整度恰恰是一个需要重点关注的内容。
根据Martin Fowler对微服务架构的描述中,虽然该架构相较于单体架构有模块化解耦、可独立部署、技术多样性等诸多优点,但是由于分布式环境下解耦,也带出了不少测试与运维复杂度。
根据微服务架构在各方面的要素,看看Spring Cloud和Dubbo都提供了哪些支持。

注意:Dubbo除了ZooKeeper,还支持两种简单的注册中心,Spring Cloud现在同时支持Eureka和ZooKeeper.Dubbo当中显示的无是指框架当中没有包含,但并不意味着无法集成。
以上列举了一些核心部件,大致可以理解为什么之前说Dubbo只是类似Netflix的一个子集了吧。当然这里需要申明一点,Dubbo对于上表中总结为“无”的组件不代表不能实现,而只是Dubbo框架自身不提供,需要另外整合以实现对应的功能,比如:
虽然,Dubbo自身只是实现了服务治理的基础,其他为保证集群安全、可维护、可测试等特性方面都没有很好的实现,但是几乎大部分关键组件都能找到第三方开源来实现,这些组件主要来自于国内各家大型互联网企业的开源产品。

RPC vs REST

另外,由于Dubbo是基础框架,其实现的内容对于我们实施微服务架构是否合理,也需要我们根据自身需求去考虑是否要修改,比如Dubbo的服务调用是通过RPC实现的,但是如果仔细拜读过Martin Fowler的microservices一文,其定义的服务间通信是HTTP协议的REST API。那么这两种有何区别呢?
先来说说,使用Dubbo的RPC来实现服务间调用的一些痛点:
相信这些痛点也是为什么当当网在dubbox(基于Dubbo的开源扩展)中增加了对REST支持的原因之一。
小结:Dubbo实现了服务治理的基础,但是要完成一个完备的微服务架构,还需要在各环节去扩展和完善以保证集群的健康,以减轻开发、测试以及运维各个环节上增加出来的压力,这样才能让各环节人员真正的专注于业务逻辑。而Spring Cloud依然发扬了Spring Source整合一切的作风,以标准化的姿态将一些微服务架构的成熟产品与框架揉为一体,并继承了Spring Boot简单配置、快速开发、轻松部署的特点,让原本复杂的架构工作变得相对容易上手一些(如果您读过我之前关于Spring Cloud的一些核心组件使用的文章,应该能体会这些让人兴奋而激动的特性,传送门)。所以,如果选择Dubbo请务必在各个环节做好整套解决方案的准备,不然很可能随着服务数量的增长,整个团队都将疲于应付各种架构上不足引起的困难。而如果选择Spring Cloud,相对来说每个环节都已经有了对应的组件支持,可能有些也不一定能满足你所有的需求,但是其活跃的社区与高速的迭代进度也会是你可以依靠的强大后盾。

Round 4:文档质量


Dubbo的文档可以说在国内开源框架中算是一流的,非常全,并且讲解的也非常深入,由于版本已经稳定不再更新,所以也不太会出现不一致的情况,另外提供了中文与英文两种版本,对于国内开发者来说,阅读起来更加容易上手,这也是dubbo在国内更火一些的原因吧。
Spring Cloud由于整合了大量组件,文档在体量上自然要比dubbo多很多,文档内容上还算简洁清楚,但是更多的是偏向整合,更深入的使用方法还是需要查看其整合组件的详细文档。另外由于Spring Cloud基于Spring Boot,很多例子相较于传统Spring应用要简单很多(因为自动化配置,很多内容都成了约定的默认配置),这对于刚接触的开发者可能会有些不适应,比较建议了解和学习Spring Boot之后再使用Spring Cloud,不然可能会出现很多一知半解的情况。
小结:虽然Spring Cloud的文档量大,但是如果使用Dubbo去整合其他第三方组件,实际也是要去阅读大量第三方组件文档的,所以在文档量上,我觉得区别不大。对于文档质量,由于Spring Cloud的迭代很快,难免会出现不一致的情况,所以在质量上我认为Dubbo更好一些。而对于文档语言上,Dubbo自然对国内开发团队来说更有优势。

总结


通过上面再几个环节上的分析,相信大家对Dubbo和Spring Cloud有了一个初步的了解。就我个人对这两个框架的使用经验和理解,打个不恰当的比喻:使用Dubbo构建的微服务架构就像组装电脑,各环节我们的选择自由度很高,但是最终结果很有可能因为一条内存质量不行就点不亮了,总是让人不怎么放心,但是如果你是一名高手,那这些都不是问题;而Spring Cloud就像品牌机,在Spring Source的整合下,做了大量的兼容性测试,保证了机器拥有更高的稳定性,但是如果要在使用非原装组件外的东西,就需要对其基础有足够的了解。
从目前Spring Cloud的被关注度和活跃度上来看,很有可能将来会成为微服务架构的标准框架。所以,Spring Cloud的系列文章,我会继续写下去。也欢迎各位朋友一起交流,共同进步。
【一些文章与示例的汇总】:http://git.oschina.net/didispace/SpringBoot-Learning
【转载请注明出处】:http://blog.didispace.com/microservice-framework/



Eureka的优势
1、在Eureka平台中,如果某台服务器宕机,Eureka不会有类似于ZooKeeper的选举leader的过程;客户端请求会自动切换到新的Eureka节点;当宕机的服务器重新恢复后,Eureka会再次将其纳入到服务器集群管理之中;而对于它来说,所有要做的无非是同步一些新的服务注册信息而已。所以,再也不用担心有“掉队”的服务器恢复以后,会从Eureka服务器集群中剔除出去的风险了。Eureka甚至被设计用来应付范围更广的网络分割故障,并实现“0”宕机维护需求。(多个zookeeper之间网络出现问题,造成出现多个leader,发生脑裂)当网络分割故障发生时,每个Eureka节点,会持续的对外提供服务(注:ZooKeeper不会):接收新的服务注册同时将它们提供给下游的服务发现请求。这样一来,就可以实现在同一个子网中(same side of partition),新发布的服务仍然可以被发现与访问。
2、正常配置下,Eureka内置了心跳服务,用于淘汰一些“濒死”的服务器;如果在Eureka中注册的服务,它的“心跳”变得迟缓时,Eureka会将其整个剔除出管理范围(这点有点像ZooKeeper的做法)。这是个很好的功能,但是当网络分割故障发生时,这也是非常危险的;因为,那些因为网络问题(注:心跳慢被剔除了)而被剔除出去的服务器本身是很”健康“的,只是因为网络分割故障把Eureka集群分割成了独立的子网而不能互访而已。
幸运的是,Netflix考虑到了这个缺陷。如果Eureka服务节点在短时间里丢失了大量的心跳连接(注:可能发生了网络故障),那么这个Eureka节点会进入”自我保护模式“,同时保留那些“心跳死亡“的服务注册信息不过期。此时,这个Eureka节点对于新的服务还能提供注册服务,对于”死亡“的仍然保留,以防还有客户端向其发起请求。当网络故障恢复后,这个Eureka节点会退出”自我保护模式“。所以Eureka的哲学是,同时保留”好数据“与”坏数据“总比丢掉任何”好数据“要更好,所以这种模式在实践中非常有效。
3、Eureka还有客户端缓存功能(注:Eureka分为客户端程序与服务器端程序两个部分,客户端程序负责向外提供注册与发现服务接口)。所以即便Eureka集群中所有节点都失效,或者发生网络分割故障导致客户端不能访问任何一台Eureka服务器;Eureka服务的消费者仍然可以通过Eureka客户端缓存来获取现有的服务注册信息。甚至最极端的环境下,所有正常的Eureka节点都不对请求产生相应,也没有更好的服务器解决方案来解决这种问题
时;得益于Eureka的客户端缓存技术,消费者服务仍然可以通过Eureka客户端查询与获取注册服务信息,这点很重要。
4、Eureka的构架保证了它能够成为Service发现服务。它相对与ZooKeeper来说剔除了Leader节点的选取或者事务日志机制,这样做有利于减少使用者维护的难度也保证了Eureka的在运行时的健壮性。而且Eureka就是为发现服务所设计的,它有独立的客户端程序库,同时提供心跳服务、服务健康监测、自动发布服务与自动刷新缓存的功能。但是,如果使用ZooKeeper你必须自己来实现这些功能。Eureka的所有库都是开源的,所有人都能看到与使用这些源代码,这比那些只有一两个人能看或者维护的客户端库要好。
5、维护Eureka服务器也非常的简单,比如,切换一个节点只需要在现有EIP下移除一个现有的节点然后添加一个新的就行。Eureka提供了一个web-based的图形化的运维界面,在这个界面中可以查看Eureka所管理的注册服务的运行状态信息:是否健康,运行日志等。Eureka甚至提供了Restful-API接口,方便第三方程序集成Eureka的功能。
ZooKeeper的劣势
   在分布式系统领域有个著名的CAP定理(C-数据一致性;A-服务可用性;P-服务对网络分区故障的容错性,这三个特性在任何分布式系统中不能同时满足,最多同时满足两个);ZooKeeper是个CP的,即任何时刻对ZooKeeper的访问请求能得到一致的数据结果,同时系统对网络分割具备容错性;但是它不能保证每次服务请求的可用性(注:也就是在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果)。但是别忘了,ZooKeeper是分布式协调服务,它的职责是保证数据(注:配置数据,状态数据)在其管辖下的所有服务之间保持同步、一致;所以就不难理解为什么ZooKeeper被设计成CP而不是AP特性的了,如果是AP的,那么将会带来恐怖的后果(注:ZooKeeper就像交叉路口的信号灯一样,你能想象在交通要道突然信号灯失灵的情况吗?)。而且,作为ZooKeeper的核心实现算法Zab,就是解决了分布式系统下数据如何在多个服务之间保持同步问题的。
1、对于Service发现服务来说就算是返回了包含不实的信息的结果也比什么都不返回要好;再者,对于Service发现服务而言,宁可返回某服务5分钟之前在哪几个服务器上可用的信息,也不能因为暂时的网络故障而找不到可用的服务器,而不返回任何结果。所以说,用ZooKeeper来做Service发现服务是肯定错误的,如果你这么用就惨了!
   如果被用作Service发现服务,ZooKeeper本身并没有正确的处理网络分割的问题;而在云端,网络分割问题跟其他类型的故障一样的确会发生;所以最好提前对这个问题做好100%的准备。就像Jepsen在ZooKeeper网站上发布的博客中所说:在ZooKeeper中,如果在同一个网络分区(partition)的节点数(nodes)数达不到ZooKeeper选取Leader节点的“法定人数”时,它们就会从ZooKeeper中断开,当然同时也就不能提供Service发现服务了。
2、ZooKeeper下所有节点不可能保证任何时候都能缓存所有的服务注册信息。如果ZooKeeper下所有节点都断开了,或者集群中出现了网络分割的故障(注:由于交换机故障导致交换机底下的子网间不能互访);那么ZooKeeper会将它们都从自己管理范围中剔除出去,外界就不能访问到这些节点了,即便这些节点本身是“健康”的,可以正常提供服务的;所以导致到达这些节点的服务请求被丢失了。(注:这也是为什么ZooKeeper不满足CAP中A的原因)
3、更深层次的原因是,ZooKeeper是按照CP原则构建的,也就是说它能保证每个节点的数据保持一致,而为ZooKeeper加上缓存的做法的目的是为了让ZooKeeper变得更加可靠(available);但是,ZooKeeper设计的本意是保持节点的数据一致,也就是CP。所以,这样一来,你可能既得不到一个数据一致的(CP)也得不到一个高可用的(AP)的Service发现服务了;因为,这相当于你在一个已有的CP系统上强制栓了一个AP的系统,这在本质上就行不通的!一个Service发现服务应该从一开始就被设计成高可用的才行!
4、如果抛开CAP原理不管,正确的设置与维护ZooKeeper服务就非常的困难;错误会经常发生,导致很多工程被建立只是为了减轻维护ZooKeeper的难度。这些错误不仅存在与客户端而且还存在于ZooKeeper服务器本身。Knewton平台很多故障就是由于ZooKeeper使用不当而导致的。那些看似简单的操作,如:正确的重建观察者(reestablishing watcher)、客户端Session与异常的处理与在ZK窗口中管理内存都是非常容易导致ZooKeeper出错的。同时,我们确实也遇到过ZooKeeper的一些经典bug:ZooKeeper-1159 与ZooKeeper-1576;我们甚至在生产环境中遇到过ZooKeeper选举Leader节点失败的情况。这些问题之所以会出现,在于ZooKeeper需要管理与保障所管辖服务群的Session与网络连接资源(注:这些资源的管理在分布式系统环境下是极其困难的);但是它不负责管理服务的发现,所以使用ZooKeeper当Service发现服务得不偿失
一个集群有3台机器,挂了一台后的影响是什么?挂了两台呢? 
挂了一台:挂了一台后就是收不到其中一台的投票,但是有两台可以参与投票,按照上面的逻辑,它们开始都投给自己,后来按照选举的原则,两个人都投票给其中一个,那么就有一个节点获得的票等于2,2 > (3/2)=1 的,超过了半数,这个时候是能选出leader的。
挂了两台: 挂了两台后,怎么弄也只能获得一张票, 1 不大于 (3/2)=1的,这样就无法选出一个leader了。
ZAB(ZooKeeper Atomic Broadcast ) 全称为:原子消息广播协议;ZAB可以说是在Paxos算法基础上进行了扩展改造而来的,ZAB协议设计了支持崩溃恢复,ZooKeeper使用单一主进程Leader用于处理客户端所有事务请求,采用ZAB协议将服务器数状态以事务形式广播到所有Follower上;由于事务间可能存在着依赖关系,ZAB协议保证Leader广播的变更序列被顺序的处理,:一个状态被处理那么它所依赖的状态也已经提前被处理;ZAB协议支持的崩溃恢复可以保证在Leader进程崩溃的时候可以重新选出Leader并且保证数据的完整性;
过半数(>=N/2+1) 的Follower反馈信息后,Leader将再次向集群内Follower广播Commit信息,Commit为将之前的Proposal提交;

1、项目的目录结构
2、基于zk服务注册和发现的架构图
      
3、服务端(像zk提供服务的访问地址)
[java] view plain copy
  1. package cn.zk.distribute;  
  2.   
  3. import org.apache.zookeeper.CreateMode;  
  4. import org.apache.zookeeper.KeeperException;  
  5. import org.apache.zookeeper.ZooDefs.Ids;  
  6. import org.apache.zookeeper.ZooKeeper;  
  7.   
  8. public class DistributedSystemServer {  
  9.   
  10.     private ZooKeeper zk = null;  
  11.   
  12.     private void getZkClient() throws Exception {  
  13.   
  14.         // 服务器在需求中并不需要做任何监听  
  15.         zk = new ZooKeeper(GlobalConstants.zkhosts,  
  16.                 GlobalConstants.sessionTimeout, null);  
  17.   
  18.     }  
  19.   
  20.     /** 
  21.      * 向zookeeper中的/servers下创建子节点 
  22.      *  
  23.      * @throws InterruptedException 
  24.      * @throws KeeperException 
  25.      */  
  26.     private void connectZK(String serverName, String port) throws Exception {  
  27.   
  28.         // 先创建出父节点  
  29.         if (zk.exists(GlobalConstants.parentZnodePath, false) == null) {  
  30.             zk.create(GlobalConstants.parentZnodePath, null,  
  31.                     Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);  
  32.         }  
  33.   
  34.         // 连接zk创建znode  
  35.         zk.create(GlobalConstants.parentZnodePath + "/",  
  36.                 (serverName + ":" + port).getBytes(), Ids.OPEN_ACL_UNSAFE,  
  37.                 CreateMode.EPHEMERAL_SEQUENTIAL);  
  38.         System.out.println("server " + serverName + " is online ......");  
  39.   
  40.     }  
  41.   
  42.     // 服务器的具体业务处理功能  
  43.     private void handle(String serverName) throws Exception {  
  44.         System.out.println("server " + serverName  
  45.                 + " is waiting for task process......");  
  46.         Thread.sleep(Long.MAX_VALUE);  
  47.   
  48.     }  
  49.   
  50.     public static void main(String[] args) throws Exception {  
  51.   
  52.         DistributedSystemServer server = new DistributedSystemServer();  
  53.   
  54.         // 获取与zookeeper通信的客户端连接  
  55.         server.getZkClient();  
  56.   
  57.         // 一启动就去zookeeper上注册服务器信息,参数1: 服务器的主机名 参数2:服务器的监听端口  
  58.         server.connectZK(args[0], args[1]);  
  59.   
  60.         // 进入业务逻辑处理流程  
  61.         server.handle(args[0]);  
  62.   
  63.     }  
  64.   
  65. }  

2、客户端(获取zk提供的服务地址,并调用服务)
[java] view plain copy
  1. package cn.zk.distribute;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. import org.apache.zookeeper.WatchedEvent;  
  7. import org.apache.zookeeper.Watcher;  
  8. import org.apache.zookeeper.ZooKeeper;  
  9. import org.apache.zookeeper.Watcher.Event.EventType;  
  10.   
  11. public class DistributedSystemClient {  
  12.   
  13.     private volatile List<String> servers = null;  
  14.     private ZooKeeper zk = null;  
  15.   
  16.     // 获取zk连接  
  17.     private void getZkClient() throws Exception {  
  18.   
  19.         // 服务器在需求中并不需要做任何监听  
  20.         zk = new ZooKeeper(GlobalConstants.zkhosts,  
  21.                 GlobalConstants.sessionTimeout, new Watcher() {  
  22.   
  23.                     @Override  
  24.                     public void process(WatchedEvent event) {  
  25.   
  26.                         if (event.getType() == EventType.None)  
  27.                             return;  
  28.   
  29.                         try {  
  30.                             // 获取新的服务器列表,重新注册监听  
  31.                             updateServers();  
  32.   
  33.                         } catch (Exception e) {  
  34.   
  35.                             e.printStackTrace();  
  36.                         }  
  37.   
  38.                     }  
  39.                 });  
  40.   
  41.     }  
  42.   
  43.     /** 
  44.      * 从zk中获取在线服务器信息 
  45.      */  
  46.     public void updateServers() throws Exception {  
  47.   
  48.         // 从servers父节点下获取到所有子节点,并注册监听  
  49.         List<String> children = zk.getChildren(GlobalConstants.parentZnodePath,  
  50.                 true);  
  51.   
  52.         ArrayList<String> serverList = new ArrayList<String>();  
  53.   
  54.         for (String child : children) {  
  55.   
  56.             byte[] data = zk.getData(GlobalConstants.parentZnodePath + "/"  
  57.                     + child, falsenull);  
  58.   
  59.             serverList.add(new String(data));  
  60.   
  61.         }  
  62.   
  63.         // 如果客户端是一个多线程程序,而且各个线程都会竞争访问servers列表,所以,在成员中用volatile修饰了一个servers变量  
  64.         // 而在更新服务器信息的这个方法中,是用一个临时List变量来进行更新  
  65.         servers = serverList;  
  66.   
  67.         // 将更新之后的服务器列表信息打印在控制台观察一下  
  68.         for (String server : serverList) {  
  69.   
  70.             System.out.println(server);  
  71.         }  
  72.         System.out.println("===================");  
  73.   
  74.     }  
  75.   
  76.     /** 
  77.      * 业务逻辑 
  78.      *  
  79.      * @throws InterruptedException 
  80.      */  
  81.     private void requestService() throws InterruptedException {  
  82.         Thread.sleep(Long.MAX_VALUE);  
  83.   
  84.     }  
  85.   
  86.     public static void main(String[] args) throws Exception {  
  87.   
  88.         DistributedSystemClient client = new DistributedSystemClient();  
  89.   
  90.         // 先构造一个zk的连接  
  91.         client.getZkClient();  
  92.   
  93.         // 获取服务器列表  
  94.         client.updateServers();  
  95.   
  96.         // 客户端进入业务流程,请求服务器的服务  
  97.         client.requestService();  
  98.   
  99.     }  
  100.   
  101. }  
3、用到的常量配置信息
[java] view plain copy
  1. package cn.zk.distribute;  
  2.   
  3. public class GlobalConstants {  
  4.     // zk服务器列表  
  5.     public static final String zkhosts = "192.168.2.118:2181";  
  6.     // 连接的超时时间  
  7.     public static final int sessionTimeout = 2000;  
  8.     // 服务在zk下的路径  
  9.     public static final String parentZnodePath = "/servers";  
  10.   
  11. }  
4、将服务端,导出为可以运行的jar文件
      
      
       
jar的运行
[java] view plain copy
  1. [root@localhost Desktop]# java -jar server.jar 192.168.2.11 4567  

5、将客户端,导出为可以运行的jar文件
      步骤与上面的4相同,可以参照上面的步骤过程。    

阅读目录
 

一、前言

  首先本文仅作为笔者在做一些调研之后的总结,仅提供思路,不提供源码,所以如果是想直接冲着源码来的,可以跳过此文。如果后续有机会将项目开源出来,会第一时间写新文章讲解实线细节。
  在分布式系统的构建之中,服务治理是类似血液一样的存在,一个好的服务治理平台可以大大降低协作开发的成本和整体的版本迭代效率。在服务治理之前,简单粗暴的RPC调用使用的点对点方式,完全通过人为进行配置操作决定,运维成本高(每次布置1套新的环境需要改一堆配置文件的IP),还容易出错,且整个系统运行期间的服务稳定性也无法很好的感知。
  关于服务治理网上相关的信息也是非常多,但是如何基于每个公司的当下情况去选择最合适的方案落地,是我们每个架构师或者Leader需要考虑的问题。所谓工欲善其事必先利其器,做好了服务治理,那么SOA化的推进会事半功倍,已经从技术层面天然支持了程序的水平扩展。.Neter社区下成熟的服务治理平台缺乏,我想这也是每个基于.Net技术栈公司面临的问题。2016年微软正式推出了Service Fabric,并于17年开源(https://github.com/Azure/service-fabric),但是相对Java社区常见的解决方案,这个还未得到大规模验证,所以还需谨慎对待。所以本文就通过对不同的成熟解决方案来分析,提炼出一些核心的通用准则,来分析自建一个服务治理框架需要做些什么。欢迎大家拍砖。
 

二、成熟的解决方案

  查阅的一些资料,目前的业界一些比较成熟的解决方案如下:
名称
所属公司
是否开源
资料文档
备注
Dubbo
阿里巴巴
 
HSF
阿里巴巴
目前已作为阿里云产品EDAS其中的套件开放使用
Tars
腾讯
已作为腾讯云应用框架对外提供使用
JSF
京东
 
Linkerd
CNCF
原型是Twitter所构建的一个基于scala的可扩展RPC系统Finagle
Motan
新浪微博
 
istio
谷歌、IBM、Lyft
 
  相关资料文档较为丰富的只有一个Dubbo。下面先罗列一下这些解决方案的架构设计(点击图片可跳转到图片出处)。
  1.阿里 - Dubbo
  
  2.阿里 - HSF
  
   3.腾讯 - Tars
  
  4.JSF
  5.CNCF - Linkerd
  
  6.新浪 - Motan
  
  7.istio
  
   大家可以看到,大部分(Linkerd除外、MSEC没找到架构图)方案的设计风格非常相似,都是通过库的方式在调用客户端做的服务发现。那么除了实际的RPC调用之外,主要多了这3个动作:注册、订阅、变更下发。除了这3个核心动作之外,其它的辅助操作还有统计上报、鉴权等等,这也是我们搭建一个服务治理框架需要实现的功能。从MVP的角度来说,注册、订阅、变更下发是最基础的核心功能。
 

三、剖析

   首先前文里也说了,引入服务治理是为了对整体的RPC调用进行集中化管理。对我们来说其核心价值在于,减少重复劳动、避免手动配置物理文件产生的问题、降低开发人员的技术运用成本。下面针对其中的功能点进行分别讲解。
服务的自动注册:
  这是一个服务治理框架的基础功能。大家运用WCF的时候应该感受更加明显,我们要配置一个WCF服务端的时候需要在config文件中做很多配置,甚至大部分公司其实配置都是一模一样的到处复制黏贴,整个这个过程其实是价值较低的重复性劳动。
  解决这个问题需要通过动态的感知到服务端的地址信息,然后针对该地址信息进行自动化配置或者模板化配置,让其快速可用。那么这些额外的信息保存在哪,就需要引入一个注册中心的概念来进行集中化管理。
 
客户端的自动发现:
  当我们在config文件中指定具体的IP和端口来定义远程服务的地址,或者直接在程序里硬编码远程服务地址时,本身就是一个端到端的访问方式。无法灵活的在程序运行过程中去增加或减少后端的服务节点。
  解决这个问题需要和服务注册的实现方式配套。还可以针对于不同类型的应用制定一些负载均衡的策略进行切换。
 
变更下发:
  客户端的自动发现就依赖于此下发的数据,需要及时把提供服务的节点信息变化下发到各个客户端。它面向的场景如:当我们进行一个发布的时候,先将需要发布的节点从负载均衡列表中移除,然后再进行更新,最后再添加到负载均衡列表中。这个时候避免了访问到正在发布中的程序。
  当然这点也可以基于状态检测模块去做,这样可以对服务节点的健康状态感知能力得到更好的加强。
 

四、实战

下面我们剖析一下这2个核心功能的实现。
1.注册、订阅
  通过上面可以看到,主流的注册、订阅的实现需要引入一个数据集中化的节点。如果我们想要自己建立这个节点程序,那么需要考虑高可用问题。如果图省事,可以引入一个分布式协调器(也可以理解为一个配置中心)来实现,如:ZooKeeper、Consul等。
  
2.变更下发
  如果上面的第一点选择自研,那么需要考虑通知下发的问题,一般可以通过tcp建立长连接来进行主动推送。
  如果使用Zookeeper的话,首先我们需要分别给每一个服务的提供方定义一个统一的目录,作为各个服务的根节点。然后让该服务的每个独立的进程在这个根节点下Create一个临时节点。这样,我们的调用方只需要watch根节点下的子节点变化,即可实现了后端各个服务提供节点的移除和新增。但是需要注意的是Zookeeper在连接断了之后,不会马上移除临时数据,只有当SESSIONEXPIRED之后,才会把这个会话建立的临时数据移除。因此,我们需要谨慎的设置Session_TimeOut。
 

五、服务治理的扩展

   在企业中,我们可以针对服务治理做更多的扩展。比如:
  1.基于版本号的服务管理,可以用于灰度发布。
  2.请求的复制回放,用于模拟真实的流量进行压测。
  3.给请求打标签用于实时的在线压测。
  4.更灵活的负载均衡和路由策略。
  5.内置的熔断机制,避免整个分布式系统产生雪崩效应。
 
 
 
作者:Zachary_Fan
出处:http://www.cnblogs.com/Zachary-Fan/p/service_manage_discovery.html


1.概述

  在项目业务倍增的情况下,查询效率受到影响,这里我们经过讨论,引进了分布式搜索套件——ElasticSearch,通过分布式搜索来解决当下业务上存在的问题。下面给大家列出今天分析的目录:
  • ElasticSearch 套件介绍
  • ElasticSearch 应用场景和案例
  • 平台架构
  下面开始今天的内容分享。

2.ElasticSearch 套件

2.1LogStash

  LogStash是一个开源的、免费的日志收集工具,属于Elastic家族的一员,负责将收集的日志信息输送到ElasticSearch,为ElasticSearch提供数据源。

2.2ElasticSearch

  ElasticSearch是一个开源的分布式搜索引擎,具备高可靠性,支持非常多的企业级搜索用例。像Solr4一样,是基于Lucene构建的。支持时间索引和全文检索。官网:https://www.elastic.co 它对外提供一系列基于Java和HTTP的API,用于索引、检索、修改大多数配置。

2.3 Kibana

  Kibana也是开源和免费的工具,同样也是Elastic家族的一员,它可以帮助我们汇总、分析和搜索重要数据日志,并且提供友好的Web可视化界面。它可以为LogStash和ElasticSearch提供一个可视化的Web界面。
  下面我们来看看ElasticSearch的应用场景和案例。

3.ElasticSearch 应用场景和案例

  在面对实时海量数据查询,实时搜索,全文搜索,ElasticSearch 都能够很好的去胜任,它是基于 Lucene、RESTful、分布式、面向云计算设计、实时搜索、全文搜索、稳定、高可靠、可扩展、安装和使用方便。下面给大家介绍一些场景的案例。
  这个开源的托管平台,对于我们开发者来说,并不陌生,我们基本每天都会去访问Github,而Github使用ElasticSearch来实现搜索,运行在多个集群上。由于代码搜索索引很大,Github专门指定一个集群。Github使用Elasticsearch搜索20TB的数据,包括13亿的文件和1300亿行的代码。
  Mozilla公司因Firefox而闻名,它目前使用Elasticsearch将测试的结果以JSON的格式进行存储,开发人员可以非常方便的查找BUG。
  Sony公司使用Elasticsearch作为信息搜索引擎,以提供对外界的查询响应。
  另外,还有很多企业也用到了ElasticSearch去作为一个分布式搜索引擎,这里就不一一列举了。

4.平台架构

  下面,我给大家用一个图来说明日志监控平台的架构,如下图所示:
  通过上图,我们可以清晰的看到日志平台整个流向过程,下面我给大家来解释图中的各个环节的含义。首先,多个独立的Agent,这里就是图左边的三个LogStash节点,他们负责收集不同来源的数据,由一个Indexer负责进行汇总和分析数据,在这个当中有一个中间过程,这里我们使用了Broker,用Redis来实现这部分功能,其作用充当一个缓冲区,之后由ElasticSearch负责存储和搜索数据,最后由前段的Kibana可视化我们收集的数据。
  这里说明几点需要注意的地方:

5.总结

  这篇博客只是给大家入个门,让大家通过一个日志监控平台的案例去熟悉ElasticSearch套件的使用,以及它的背景。后面我会专门用于一个ElasticSearch实战系列,来给大家分析这部分内容,包括平台的搭建部署,到平台的实现这一整个流程,这篇文章大家能够有个印象,熟悉各个套件的作用即可。

6.结束语

  这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!


2.6 API Gateway

API Gateway是微服务架构中不可或缺的部分。API Gateway的定义以及存在的意义,Chris已经为大家描述过了,本文不再赘述,以下是链接:
中文版:http://dockone.io/article/482
英文版:https://www.nginx.com/blog/building-microservices-using-an-api-gateway/
使用API Gateway后,客户端和微服务之间的网络图变成下图:
通过API Gateway,可以统一向外部系统提供REST API。Spring Cloud中使用Zuul作为API Gateway。Zuul提供了动态路由、监控、回退、安全等功能。
下面我们进入Zuul的学习:

准备工作

127.0.0.1 gateway

Zuul代码示例

创建Maven项目,在pom.xml中添加如下内容:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<artifactId>microservice-api-gateway</artifactId>
<packaging>jar</packaging>

<parent>
<groupId>com.itmuch.cloud</groupId>
<artifactId>spring-cloud-microservice-study</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies></project>
启动类:
/**
* 使用@EnableZuulProxy注解激活zuul。
* 跟进该注解可以看到该注解整合了@EnableCircuitBreaker@EnableDiscoveryClient,是个组合注解,目的是简化配置。
* @author eacdy
*/@SpringBootApplication@EnableZuulProxypublic class ZuulApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApiGatewayApplication.class, args);
}
}
配置文件:application.yml
spring: application: name: microservice-api-gateway
server: port: 8050eureka: instance: hostname: gateway
client: serviceUrl: defaultZone:http://discovery:8761/eureka/
这样,一个简单的API Gateway就完成了。

测试

启动microservice-api-gateway项目。还记得我们之前访问通过http://localhost:8000/1去访问microservice-provider-user服务中id=1的用户信息吗?
我们现在访问http://localhost:8050/microservice-provider-user/1试试。会惊人地看到:
{"id":1,"username":"Tom","age":12}
这不正是microservice-provider-user服务中id=1的用户信息吗?
所以我们可以总结出规律:访问
http://GATEWAY:GATEWAY_PORT/想要访问的Eureka服务id的小写/**
,将会访问到
http://想要访问的Eureka服务id的小写:该服务端口/**

自定义路径

上文我们已经完成了通过API Gateway去访问微服务的目的,是通过
http://GATEWAY:GATEWAY_PORT/想要访问的Eureka服务id的小写/**
的形式访问的,那么如果我们想自定义在API Gateway中的路径呢?譬如想使用
http://localhost:8050/user/1
就能够将请求路由到http://localhost:8000/1呢?
只需要做一点小小的配置即可:
spring: application: name: microservice-api-gateway
server: port: 8050eureka: instance: hostname: gateway
client: serviceUrl: defaultZone:http://discovery:8761/eureka/
# 下面整个树都非必须,如果不配置,将默认使用 http://GATEWAY:GATEWAY_PORT/想要访问的Eureka服务id的小写/** 路由到:http://想要访问的Eureka服务id的小写:该服务端口/**zuul: routes: user: # 可以随便写,在zuul上面唯一即可;当这里的值 = service-id时,service-id可以不写。 path: /user/** # 想要映射到的路径 service-id: microservice-provider-user # Eureka中的serviceId

如何忽略某些服务

准备工作

  1. 启动服务:microservice-discovery-eureka
  2. 启动服务:microservice-provider-user
  3. 启动服务:microservice-consumer-movie-ribbon
如果我们现在只想将microservice-consumer-movie-ribbon服务暴露给外部,microservice-provider-user不想暴露,那么应该怎么办呢?
依然只是一点小小的配置即可:
spring: application: name: microservice-api-gateway
server: port: 8050eureka: instance: hostname: gateway
client: serviceUrl: defaultZone:http://discovery:8761/eureka/
zuul: ignored-services: microservice-provider-user # 需要忽视的服务(配置后将不会被路由) routes: movie: # 可以随便写,在zuul上面唯一即可;当这里的值 = service-id时,service-id可以不写。 path: /movie/** # 想要映射到的路径 service-id: microservice-consumer-movie-ribbon-with-hystrix # Eureka中的serviceId
这样microservice-provider-user服务就不会被路由,microservice-consumer-movie-ribbon服务则会被路由。
测试结果:
URL
结果
备注
404
说明microservice-provider-user未被路由
{"id":1,"username":"Tom","age":12}
说明microservice-consumer-movie-ribbon被路由了。

使用Zuul不使用Eureka

Zuul并不依赖Eureka,可以脱离Eureka运行,此时需要配置
spring: application: name: microservice-api-gateway
server: port: 8050zuul: routes: movie: # 可以随便写 path: /user/**
url:http://localhost:8000/ # path路由到的地址,也就是访问http://localhost:8050/user/**会路由到http://localhost:8000/**
我们可尝试访问http://localhost:8050/user/1 ,会发现被路由到了http://localhost:8000/1 。不过在大多数情况下,笔者并不建议这么做,因为得手动大量地配置URL,不是很方便。

其他使用

Zuul还支持更多的特性、更多的配置项甚至是定制开发,具体还请读者自行发掘。

同类软件

Zuul只是API Gateway的一种实现,可作为API Gateway的软件有很多,譬如Nginx Plus、Kong等等,本文不做赘述了。

参考文档

https://www.nginx.com/blog/building-microservices-using-an-api-gateway/http://microservices.io/patterns/apigateway.html

前言

最近很久没有写博客了,一方面是因为公司事情最近比较忙,另外一方面是因为在进行 CAP 的下一阶段的开发工作,不过目前已经告一段落了。
接下来还是开始我们今天的话题,说说分布式事务,或者说是我眼中的分布式事务,因为每个人可能对其的理解都不一样。
分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免,本文就分布式事务来简单聊一下。

数据库事务

在说分布式事务之前,我们先从数据库事务说起。 数据库事务可能大家都很熟悉,在开发过程中也会经常使用到。但是即使如此,可能对于一些细节问题,很多人仍然不清楚。比如很多人都知道数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation)和持久性(Durabilily),简称就是ACID。但是再往下比如问到隔离性指的是什么的时候可能就不知道了,或者是知道隔离性是什么但是再问到数据库实现隔离的都有哪些级别,或者是每个级别他们有什么区别的时候可能就不知道了。
本文并不打算介绍这些数据库事务的这些东西,有兴趣可以搜索一下相关资料。不过有一个知识点我们需要了解,就是假如数据库在提交事务的时候突然断电,那么它是怎么样恢复的呢? 为什么要提到这个知识点呢? 因为分布式系统的核心就是处理各种异常情况,这也是分布式系统复杂的地方,因为分布式的网络环境很复杂,这种“断电”故障要比单机多很多,所以我们在做分布式系统的时候,最先考虑的就是这种情况。这些异常可能有 机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失、其他异常等等...
我们接着说本地事务数据库断电的这种情况,它是怎么保证数据一致性的呢?我们使用SQL Server来举例,我们知道我们在使用 SQL Server 数据库是由两个文件组成的,一个数据库文件和一个日志文件,通常情况下,日志文件都要比数据库文件大很多。数据库进行任何写入操作的时候都是要先写日志的,同样的道理,我们在执行事务的时候数据库首先会记录下这个事务的redo操作日志,然后才开始真正操作数据库,在操作之前首先会把日志文件写入磁盘,那么当突然断电的时候,即使操作没有完成,在重新启动数据库时候,数据库会根据当前数据的情况进行undo回滚或者是redo前滚,这样就保证了数据的强一致性。
接着,我们就说一下分布式事务。

分布式理论

当我们的单个数据库的性能产生瓶颈的时候,我们可能会对数据库进行分区,这里所说的分区指的是物理分区,分区之后可能不同的库就处于不同的服务器上了,这个时候单个数据库的ACID已经不能适应这种情况了,而在这种ACID的集群环境下,再想保证集群的ACID几乎是很难达到,或者即使能达到那么效率和性能会大幅下降,最为关键的是再很难扩展新的分区了,这个时候如果再追求集群的ACID会导致我们的系统变得很差,这时我们就需要引入一个新的理论原则来适应这种集群的情况,就是 CAP 原则或者叫CAP定理,那么CAP定理指的是什么呢?

CAP定理

CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:
具体地讲在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。
这个定理在迄今为止的分布式系统中都是适用的! 为什么这么说呢?
这个时候有同学可能会把数据库的2PC(两阶段提交)搬出来说话了。OK,我们就来看一下数据库的两阶段提交。
对数据库分布式事务有了解的同学一定知道数据库支持的2PC,又叫做 XA Transactions。
MySQL从5.5版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。
其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:
其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。这样做的缺陷是什么呢? 咋看之下我们可以在数据库分区之间获得一致性。
如果CAP 定理是对的,那么它一定会影响到可用性。
如果说系统的可用性代表的是执行某项操作相关所有组件的可用性的和。那么在两阶段提交的过程中,可用性就代表了涉及到的每一个数据库中可用性的和。我们假设两阶段提交的过程中每一个数据库都具有99.9%的可用性,那么如果两阶段提交涉及到两个数据库,这个结果就是99.8%。根据系统可用性计算公式,假设每个月43200分钟,99.9%的可用性就是43157分钟, 99.8%的可用性就是43114分钟,相当于每个月的宕机时间增加了43分钟。
以上,可以验证出来,CAP定理从理论上来讲是正确的,CAP我们先看到这里,等会再接着说。

BASE理论

在分布式系统中,我们往往追求的是可用性,它的重要程序比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
有了以上理论之后,我们来看一下分布式事务的问题。

分布式事务

在分布式系统中,要实现分布式事务,无外乎那几种解决方案。

一、两阶段提交(2PC)

和上一节中提到的数据库XA事务一样,两阶段提交就是使用XA协议的原理,我们可以从下面这个图的流程来很容易的看出中间的一些比如commit和abort的细节。
两阶段提交这种解决方案属于牺牲了一部分可用性来换取的一致性。在实现方面,在 .NET 中,可以借助 TransactionScop 提供的 API 来编程实现分布式系统中的两阶段提交,比如WCF中就有实现这部分功能。不过在多服务器之间,需要依赖于DTC来完成事务一致性,Windows下微软搞的有MSDTC服务,Linux下就比较悲剧了。
另外说一句,TransactionScop 默认不能用于异步方法之间事务一致,因为事务上下文是存储于当前线程中的,所以如果是在异步方法,需要显式的传递事务上下文。
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景,如果分布式系统跨接口调用,目前 .NET 界还没有实现方案。

二、补偿事务(TCC)

TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
举个例子,假入 Bob 要向 Smith 转账,思路大概是:
我们有一个本地方法,里面依次调用
1、首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。
2、在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。

三、本地消息表(异步确保)

本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。我们可以从下面的流程图中看出其中的一些细节:
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。在 .NET中 有现成的解决方案。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

四、MQ 事务消息

有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
遗憾的是,RocketMQ并没有 .NET 客户端。有关 RocketMQ的更多消息,大家可以查看这篇博客
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 实现难度大,主流MQ不支持,没有.NET客户端,RocketMQ事务消息部分代码也未开源。

五、Sagas 事务模型

Saga事务模型又叫做长时间运行的事务(Long-running-transaction), 它是由普林斯顿大学的H.Garcia-Molina等人提出,它描述的是另外一种在没有两阶段提交的的情况下解决分布式系统中复杂的业务事务问题。你可以在这里看到 Sagas 相关论文。
我们这里说的是一种基于 Sagas 机制的工作流事务模型,这个模型的相关理论目前来说还是比较新的,以至于百度上几乎没有什么相关资料。
该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由 Sagas 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。
比如我们一次关于购买旅游套餐业务操作涉及到三个操作,他们分别是预定车辆,预定宾馆,预定机票,他们分别属于三个不同的远程接口。可能从我们程序的角度来说他们不属于一个事务,但是从业务角度来说是属于同一个事务的。
他们的执行顺序如上图所示,所以当发生失败时,会依次进行取消的补偿操作。
因为长事务被拆分了很多个业务流,所以 Sagas 事务模型最重要的一个部件就是工作流或者你也可以叫流程管理器(Process Manager),工作流引擎和Process Manager虽然不是同一个东西,但是在这里,他们的职责是相同的。在选择工作流引擎之后,最终的代码也许看起来是这样的
SagaBuilder saga = SagaBuilder.newSaga("trip")
.activity("Reserve car", ReserveCarAdapter.class)
.compensationActivity("Cancel car", CancelCarAdapter.class)
.activity("Book hotel", BookHotelAdapter.class)
.compensationActivity("Cancel hotel", CancelHotelAdapter.class)
.activity("Book flight", BookFlightAdapter.class)
.compensationActivity("Cancel flight", CancelFlightAdapter.class)
.end()
.triggerCompensationOnAnyError();

camunda.getRepositoryService().createDeployment()
.addModelInstance(saga.getModel())
.deploy();
这里有一个 C# 相关示例,有兴趣的同学可以看一下。
优缺点这里我们就不说了,因为这个理论比较新,目前市面上还没有什么解决方案,即使是 Java 领域,我也没有搜索的太多有用的信息。

分布式事务解决方案:CAP

上面介绍的那些分布式事务的处理方案你在其他地方或许也可以看到,但是并没有相关的实际代码或者是开源代码,所以算不上什么干货,下面就放干货了。
在 .NET 领域,似乎没有什么现成的关于分布式事务的解决方案,或者说是有但未开源。具笔者了解,有一些公司内部其实是有这种解决方案的,但是也是作为公司的一个核心产品之一,并未开源...
鉴于以上原因,所以博主就打算自己写一个并且开源出来,所以从17年初就开始做这个事情,然后花了大半年的时间在一直不断完善,就是下面这个 CAP。
Github CAP:这里的 CAP 就不是 CAP 理论了,而是一个 .NET 分布式事务解决方案的名字。
详细介绍:
http://www.cnblogs.com/savorboard/p/cap.html
相关文档:
http://www.cnblogs.com/savorboard/p/cap-document.html
夸张的是,这个解决方案是具有可视化界面(Dashboard)的,你可以很方面的看到哪些消息执行成功,哪些消息执行失败,到底是发送失败还是处理失败,一眼便知。
最夸张的是,这个解决方案的可视化界面还提供了实时动态图表,这样不但可以看到实时的消息发送及处理情况,连当前的系统处理消息的速度都可以看到,还可以看到过去24小时内的历史消息吞吐量。
最最夸张的是,这个解决方案的还帮你集成了 Consul 做分布式节点发现和注册还有心跳检查,你随时可以看到其他的节点的状况。
最最最夸张的是,你以为你看其他节点的数据要登录到其他节点的Dashboard控制台看?错了,你随便打开其中任意一个节点的Dashboard,点一下就可以切换到你想看的节点的控制台界面了,就像你看本地的数据一样,他们是完全去中心化的。
你以为这些就够了?不,远远不止:
这下你以为我说完了? 不!
你完全可以把 CAP 当做一个 EventBus 来使用,CAP具有优秀的消息处理能力,不要担心瓶颈会在CAP,那是永远不可能, 因为你随时可以在配置中指定CAP处理的消息使用的进程数, 只要你的数据库配置足够高...
说了这么多,口干舌燥的,你不 Star 一下给个精神上的支持说不过去吧? ^_^
2号传送门: https://github.com/dotnetcore/CAP
不 Star 也没关系,我选择原谅你~

总结

通过本文我们了解到两个分布式系统的理论,他们分别是CAP和BASE 理论,同时我们也总结并对比了几种分布式分解方案的优缺点,分布式事务本身是一个技术难题,是没有一种完美的方案应对所有场景的,具体还是要根据业务场景去抉择吧。 然后我们介绍了一种基于本地消息的的分布式事务解决方案CAP。
如果你觉得本篇文章对您有帮助的话,感谢您的【推荐】。
如果你对 .NET Core 有兴趣的话可以关注我,我会定期的在博客分享我的学习心得。

本文地址:http://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html
作者博客:Savorboard
欢迎转载,请在明显位置给出出处及链接


个人总结
主机根据ip地址和虚拟机的编号 host_ip#id生成hash值,排列成环。
需要缓存的对象也用同样的hash算法生成hash值,排列成环。
然后对象存储在顺时针离它最近的主机上。
单调性体现在: 
无论是新增主机还是删除主机,需要改变位置的都是离那台主机最近的那些节点,其他节点不需要改变位置。
原文
地址:http://www.jianshu.com/p/e8fb89bb3a61

基本场景

比如你有 N 个 cache 服务器(后面简称 cache ),那么如何将一个对象 object 映射到 N 个 cache 上呢,你很可能会采用类似下面的通用方法计算 object 的 hash 值,然后均匀的映射到到 N 个 cache ;
求余算法: hash(object)%N
一切都运行正常,再考虑如下的两种情况;
1 一个 cache 服务器 m down 掉了(在实际应用中必须要考虑这种情况),这样所有映射到 cache m 的对象都会失效,怎么办,需要把 cache m 从 cache 中移除,这时候 cache 是 N-1 台,映射公式变成了 hash(object)%(N-1) ;
2 由于访问加重,需要添加 cache ,这时候 cache 是 N+1 台,映射公式变成了 hash(object)%(N+1) ;
1 和 2 意味着什么?这意味着突然之间几乎所有的 cache 都失效了。对于服务器而言,这是一场灾难,洪水般的访问都会直接冲向后台服务器;
再来考虑第三个问题,由于硬件能力越来越强,你可能想让后面添加的节点多做点活,显然上面的 hash 算法也做不到。
有什么方法可以改变这个状况呢,这就是 consistent hashing...

hash 算法和单调性

   Hash 算法的一个衡量指标是单调性( Monotonicity ),定义如下:
单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
容易看到,上面的简单求余算法 hash(object)%N 难以满足单调性要求。

Consistent Hashing 一致性hash的原理

consistent hashing 是一种 hash 算法,简单的说,在移除 / 添加一个 cache 时,它能够尽可能小的改变已存在key 映射关系,尽可能的满足单调性的要求。
1. 环形hash 空间
考虑通常的 hash 算法都是将 value 映射到一个 32 为的 key 值,也即是 0~2^32-1 次方的数值空间;我们可以将这个空间想象成一个首( 0 )尾( 2^32-1 )相接的圆环,如下面图 1 所示的那样。
circle space
2. 把需要缓存的内容(对象)映射到hash 空间
接下来考虑 4 个对象 object1~object4 ,通过 hash 函数计算出的 hash 值 key 在环上的分布如图 2 所示。
hash(object1) = key1;
… …
hash(object4) = key4;
object
3 .把服务器(节点)映射到hash 空间
Consistent hashing 的基本思想就是将对象和 cache 都映射到同一个 hash 数值空间中,并且使用相同的 hash算法。
假设当前有 A,B 和 C 共 3 台服务器(节点),那么其映射结果将如图 3 所示,他们在 hash 空间中,以对应的 hash 值排列。
一般的方法可以使用 服务器(节点) 机器的 IP 地址或者机器名作为 hash输入。
hash(cache A) = key A;
… …
hash(cache C) = key C;
cache
4 .把对象映射到cache
现在cache和对象都已经通过同一个 hash 算法映射到 hash 数值空间中了,接下来要考虑的就是如何将对象映射到 cache 上面了。
在这个环形空间中,如果沿着顺时针方向从对象的 key 值出发,直到遇见一个 cache ,那么就将该对象存储在这个 cache 上,因为对象和 cache 的 hash 值是固定的,因此这个 cache 必然是唯一和确定的。这样不就找到了对象和 cache 的映射方法了吗?!
依然继续上面的例子,那么根据上面的方法,对象 object1 将被存储到 cache A 上; object2 和object3 对应到 cache C ; object4 对应到 cache B ;
5. 考察cache 的变动
前面讲过,通过 hash 然后求余的方法带来的最大问题就在于不能满足单调性,当 cache 有所变动时, cache会失效,进而对后台服务器造成巨大的冲击,现在就来分析分析 consistent hashing 算法。
因此这里仅需要变动对象 object2 ,将其重新映射到 cache D 上;参见图 5 。
图 5 添加 cache D 后的映射关系
6 .虚拟节点
考量 Hash 算法的另一个指标是平衡性 (Balance) ,定义如下:
平衡性
  平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。
hash 算法并不是保证绝对的平衡,如果 cache 较少的话,对象并不能被均匀的映射到 cache 上,比如在上面的例子中,仅部署 cache A 和 cache C 的情况下,在 4 个对象中, cache A 仅存储了 object1 ,而 cache C 则存储了object2 、 object3 和 object4 ;分布是很不均衡的。
为了解决这种情况, consistent hashing 引入了“虚拟节点”的概念,它可以如下定义:
“虚拟节点”( virtual node )是实际节点在 hash 空间的复制品( replica ),一实际个节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。
仍以仅部署 cache A 和 cache C 的情况为例,在图 4 中我们已经看到, cache 分布并不均匀。现在我们引入虚拟节点,并设置“复制个数”为 2 ,这就意味着一共会存在 4 个“虚拟节点”, cache A1, cache A2 代表了 cache A; cache C1, cache C2 代表了 cache C ;假设一种比较理想的情况,参见图 6 。
图 6 引入“虚拟节点”后的映射关系
此时,对象到“虚拟节点”的映射关系为:
objec1->cache A2 ; objec2->cache A1 ; objec3->cache C1 ; objec4->cache C2 ;
因此对象 object1 和 object2 都被映射到了 cache A 上,而 object3 和 object4 映射到了 cache C 上;平衡性有了很大提高。
引入“虚拟节点”后,映射关系就从 { 对象 -> 节点 } 转换到了 { 对象 -> 虚拟节点 } 。查询物体所在 cache 时的映射关系如图 7 所示。
图 7 查询对象所在 cache
“虚拟节点”的 hash 计算可以采用对应节点的 IP 地址加数字后缀的方式。例如假设 cache A 的 IP 地址为202.168.14.241 。
引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“202.168.14.241”);
引入“虚拟节点”后,计算“虚拟节”点 cache A1 和 cache A2 的 hash 值:
Hash(“202.168.14.241#1”); // cache A1
Hash(“202.168.14.241#2”); // cache A2

以下时代码实现DEMO

#!/usr/bin/env python# -*- coding: utf-8 -*-from zlib import crc32import memcacheclass HashConsistency(object): def__init__(self, nodes=None, replicas=5): # 虚拟节点与真实节点对应关系 self.nodes_map = [] # 真实节点与虚拟节点的字典映射self.nodes_replicas = {} # 真实节点 self.nodes = nodes # 每个真实节点创建的虚拟节点的个数 self.replicas = replicas ifself.nodes: for node in self.nodes: self._add_nodes_map(node) self._sort_nodes() def get_node(self, key): """ 根据KEY值的hash值,返回对应的节点 算法是: 返回最早比key_hash大的节点 """ key_hash = abs(crc32(key)) #print '(%s' % key_hash fornode in self.nodes_map: if key_hash > node[0]: continue return node return None def add_node(self, node): # 添加节点self._add_nodes_map(node) self._sort_nodes() def remove_node(self, node): # 删除节点 if node not inself.nodes_replicas.keys(): pass discard_rep_nodes = self.nodes_replicas[node] self.nodes_map = filter(lambda x: x[0] notin discard_rep_nodes, self.nodes_map) def _add_nodes_map(self, node): # 增加虚拟节点到nodes_map列表 nodes_reps = [] for iin xrange(self.replicas): rep_node = '%s_%d' % (node, i) node_hash = abs(crc32(rep_node)) self.nodes_map.append((node_hash, node)) nodes_reps.append(node_hash) # 真实节点与虚拟节点的字典映射self.nodes_replicas[node] = nodes_reps def _sort_nodes(self): # 按顺序排列虚拟节点 self.nodes_map = sorted(self.nodes_map, key=lambda x:x[0])memcache_servers = [ '127.0.0.1:7001', '127.0.0.1:7002', '127.0.0.1:7003','127.0.0.1:7004',]h = HashConsistency(memcache_servers)for k in h.nodes_map: print kmc_servers_dict = {}for ms inmemcache_servers: mc = memcache.Client([ms], debug=0) mc_servers_dict[ms] = mc# 循环10此给memcache 添加key,这里使用了一致性hash,那么key将会根据hash值落点到对应的虚拟节点上for i in xrange(10): key = 'key_%s' % i print key server = h.get_node(key)[1] mc = mc_servers_dict[server] mc.set(key, i) print 'SERVER :%s' % server print mc

java代码如下 

import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

import com.fhr.consisthash.util.FNV1HashUtils;

/**
* 一致性哈希算法实现
*
* created on 2018年3月10日
*
* @author fhr
*/
public class ConsistencyHashServer {
    // 缺省每个真实节点创建的虚拟节点个数 虚拟节点的目的是为了更加平均 实际引用当中性能更好的服务器可以增加虚拟节点数量
    private static final int DEF_REPLICAS = 5;

    // 真实结点列表,考虑到服务器上线、下线的场景,即添加、删除的场景会比较频繁,这里使用LinkedList会更好
    private final List<String> realNodes = new LinkedList<String>();

    // 虚拟节点,key表示虚拟节点的hash值,value表示虚拟节点的名称 红黑树
    private final SortedMap<Integer, String> virtualNodes = new TreeMap<Integer, String>();

    // 构造方法当中初始化
    public ConsistencyHashServer(String[] servers) {
        // 先把原始的服务器添加到真实结点列表中
        for (int i = 0; i < servers.length; i++) {
            realNodes.add(servers[i]);
        }
        // 再添加虚拟节点,遍历LinkedList使用foreach循环效率会比较高
        for (String str : realNodes) {
            for (int i = 0; i < DEF_REPLICAS; i++) {
                String virtualNodeName = str + "&&VN" + String.valueOf(i);
                int hash = FNV1HashUtils.getHash(virtualNodeName);
                System.out.println("虚拟节点[" + virtualNodeName + "]被添加, hash值为" + hash);
                virtualNodes.put(hash, virtualNodeName);
            }
        }
    }

    // 获取对象的对应服务器节点
    public String getServer(String key) {
        // 得到该key的hash
        int hash = FNV1HashUtils.getHash(key);
        // 得到大于该Hash值的所有Map
        SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
        // 第一个Key就是顺时针过去离node最近的那个结点
        Integer i = subMap.firstKey();
        // 返回对应的虚拟节点名称,这里字符串稍微截取一下
        String virtualNode = subMap.get(i);
        return virtualNode.substring(0, virtualNode.indexOf("&&"));
    }

}
public class FNV1HashUtils {
     
     // 使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别
     public static int getHash(String str) {
          final int p = 16777619;
          int hash = (int) 2166136261L;
          for (int i = 0; i < str.length(); i++) {
              hash = (hash ^ str.charAt(i)) * p;
          }
          hash += hash << 13;
          hash ^= hash >> 7;
          hash += hash << 3;
          hash ^= hash >> 17;
          hash += hash << 5;
          // 如果算出来的值为负数则取其绝对值
          if (hash < 0) {
              hash = Math.abs(hash);
          }
          return hash;
     }
}

class ConsistencyHashServerTest {
     @Test
     public void test() {
          // 服务器节点
          final String[] servers = { "192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111", "192.168.0.3:111",
                   "192.168.0.4:111" };
          final ConsistencyHashServer consistencyHashServer = new ConsistencyHashServer(servers);
          // 需要存储的值
          final String[] keys = { "key1", "key2", "key3" };
          // 依次求出具体存储的服务器节点
          for (String key : keys) {
              final int hash = FNV1HashUtils.getHash(key);
              final String node = consistencyHashServer.getServer(key);
              System.out.println(String.format("[%s]的hash值为%d,被路由到服务器节点[%s]", key, hash, node));
          }
     }
}




Ribbon介绍

Ribbon核心概念是命名的客户端.每个负载均衡器是共同组件的集合的一部分,通过远程服务器联系, 你把它作为应用程序开发者(例如,使用 @FeignClient注解)的名称,Spring Cloud创建一个新的整体使用RibbonClientConfiguration为每一个客户端命名的 ApplicationContext,这包含(除其他事项外)的ILoadBalancer,一个RestClient 实现和ServerListFilter

使用Ribbon做负载均衡

当Ribbon与Eureka联合使用时,RibbonServerList会被DiscoveryEnabledNIWSServerList重写,扩展成从Eureka注册中心中获取服务端列表。同时它也会用NIWSDiscoveryPing来取代IPing,它将职责委托给Eureka来确定服务端是否已经启动。

开发案例

首先我们需要用到上一节中用到的eureka的server和client,我们需要再复制一份client,修改端口为8081.
此时我们有
Eureka server http://localhost:8761
Eureka client1 http://localhost:8080
eureka client2 http://localhost:8081

配置Ribbon

pom.xml

引入Ribbon的maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Dalston.RELEASE</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-Ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

application.xml

配置Ribbon服务的相关信息(端口,eureka等)
spring:
application:
name: Ribbon-demo
server:
port: 8082
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

application.java

@SpringBootApplication@EnableDiscoveryClientpublic class SpringCloudNetflixRibbonApplication {

@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(SpringCloudNetflixRibbonApplication.class, args);
}
}
通过@EnableDiscoveryClient注解开启服务发现功能,@LoadBalanced注解开启均衡负载.至于@LoadBalanced的原理剖析,请参考这篇文章Ribbon源码剖析

controller

@RestControllerpublic class TestController {

@Autowired
private RestTemplate restTemplate;

@RequestMapping(value = "/", method = RequestMethod.GET)
public String info() {
return restTemplate.getForEntity("http://eureka-client/", String.class).getBody();
}
}
我们通过restTemplate去调度我们在eureka server中注册的服务.
此处的eureka-client需要和我们在eureka client中配置的名称一致,不然会导致出现如下错误:
No instances available for xxx
让我们依次开启eureka server,eurek client,Ribbon的服务.在网址中输入http://localhost:8761
可以看到eureka client,Ribbon都已经在eureka server中处于UP状态.
然后我们在调用 http://localhost:8082
分别在两个client中出现如下日志
2017-05-01 10:05:47.021 INFO 15240 — [nio-8081-exec-1] ingCloudDiscoveryEurekaClientApplication : hello world,this is eureka client2
2017-05-01 10:06:33.211 INFO 3552 — [nio-8080-exec-2] ingCloudDiscoveryEurekaClientApplication : hello world,this is eureka client1
说明Ribbon的负载均衡效果是有效的.
本案例只是简单的介绍Ribbon的负载均衡能力,更多有深度的东西,还需要在后续的学习中研究.

Feign介绍

Feign 是一个声明web服务客户端,这使得编写web服务客户端更容易,使用Feign 创建一个接口并对它进行注解,它具有可插拔的注解支持包括Feign注解与JAX-RS注解,Feign还支持可插拔的编码器与解码器,Spring Cloud 增加了对 Spring MVC的注解,Spring Web 默认使用了HttpMessageConverters, Spring Cloud 集成 Ribbon 和 Eureka 提供的负载均衡的HTTP客户端 Feign.
通过Feign也可以做到和Ribbon一样的负载均衡功能,配置和上面的基本一致,我们只需要将Ribbon的服务缓存Feign的服务即可.具体配置如下

Feign案例

pom.xml

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Dalston.RELEASE</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-Feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

application.yml

spring:
application:
name: Feign-demo
server:
port: 8083
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
和Ribbon的配置类似,都是配置服务中心等一些基本中心

application.java

@SpringBootApplication@EnableFeignClients@EnableEurekaClientpublic class SpringCloudNetflixFeignClientApplication {

public static void main(String[] args) {
SpringApplication.run(SpringCloudNetflixFeignClientApplication.class, args);
}
}
@EnableFeignClients注解表示开启Feign的功能.@EnableEurekaClient开启eureka客户端支持

FeignClient

@FeignClient("eureka-client")
public interface TestClient {
@RequestMapping(method = RequestMethod.GET, value = "/")
String getValue(String info);
}
@FeignClient注释中,String值(以上“eureka-client”)是一个任意的客户端名称,用于创建功能区负载平衡器,和我们eureka的client名称要一致,不然会找不到服务.

controller

@RestControllerpublic class TestController {

@Autowired
private TestClient testClient;

@RequestMapping("/")
public String getInfo(){
return testClient.getValue("hello world");
}

}
和上面的Ribbon的一样,依次开启eureka server,eureka client,Feign服务,然后在浏览器输入 http://localhost:8083两次
会出现和Ribbon一样的结果,这里就不再多描述.

结语

Ribbon和Feign的负载功能介绍到此结束,更多深入的内容我们需要去读他们的文档.
本文的demo已经放在github,地址https://github.com/eumji025/spring-cloud-repository
与君共勉!!!

SpringCloud分布式开发五大利器

  • 服务发现——Netflix Eureka
  • 客服端负载均衡——Netflix Ribbon
  • 断路器——Netflix Hystrix
  • 服务网关——Netflix Zuul
  • 分布式配置——Spring Cloud Config

Eureka

一个RESTful服务,用来定位运行在AWS地区(Region)中的中间层服务。由两个组件组成:Eureka服务器和Eureka客户端。Eureka服务器用作服务注册服务器。Eureka客户端是一个java客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。Netflix在其生产环境中使用的是另外的客户端,它提供基于流量、资源利用率以及出错状态的加权负载均衡。

Ribbon

Ribbon,主要提供客户侧的软件负载均衡算法。
Ribbon客户端组件提供一系列完善的配置选项,比如连接超时、重试、重试算法等。Ribbon内置可插拔、可定制的负载均衡组件。下面是用到的一些负载均衡策略:
  • 简单轮询负载均衡
  • 加权响应时间负载均衡
  • 区域感知轮询负载均衡
  • 随机负载均衡
Ribbon中还包括以下功能:
  • 易于与服务发现组件(比如Netflix的Eureka)集成
  • 使用Archaius完成运行时配置
  • 使用JMX暴露运维指标,使用Servo发布
  • 多种可插拔的序列化选择
  • 异步和批处理操作(即将推出)
  • 自动SLA框架(即将推出)
  • 系统管理/指标控制台(即将推出)

Hystrix

断路器可以防止一个应用程序多次试图执行一个操作,即很可能失败,允许它继续而不等待故障恢复或者浪费 CPU 周期,而它确定该故障是持久的。断路器模式也使应用程序能够检测故障是否已经解决。如果问题似乎已经得到纠正,应用程序可以尝试调用操作。
断路器增加了稳定性和灵活性,以一个系统,提供稳定性,而系统从故障中恢复,并尽量减少此故障的对性能的影响。它可以帮助快速地拒绝对一个操作,即很可能失败,而不是等待操作超时(或者不返回)的请求,以保持系统的响应时间。如果断路器提高每次改变状态的时间的事件,该信息可以被用来监测由断路器保护系统的部件的健康状况,或以提醒管理员当断路器跳闸,以在打开状态。
流程图

Zuul

类似nginx,反向代理的功能,不过netflix自己增加了一些配合其他组件的特性。

Spring Cloud Config

这个还是静态的,得配合Spring Cloud Bus实现动态的配置更新。

参考



说起springcloud熔断让我想起了去年股市中的熔断,多次痛的领悟,随意实施的熔断对整个系统的影响是灾难性的,好了接下来我们还是说正事。

熔断器

雪崩效应

在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。
如果下图所示:A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。

熔断器(CircuitBreaker)

熔断器的原理很简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。
熔断器模式就像是那些容易导致错误的操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定使用允许操作继续,或者立即返回错误。 熔断器开关相互转换的逻辑如下图:
熔断器就是保护服务高可用的最后一道防线。

Hystrix特性

1.断路器机制
断路器很好理解, 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力.
2.Fallback
Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存.
3.资源隔离
在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池. 例如调用产品服务的Command放入A线程池, 调用账户服务的Command放入B线程池. 这样做的主要优点是运行环境被隔离开了. 这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽时, 不会对系统的其他服务造成影响. 但是带来的代价就是维护多个线程池会对系统带来额外的性能开销. 如果是对性能有严格要求而且确信自己调用服务的客户端代码不会出问题的话, 可以使用Hystrix的信号模式(Semaphores)来隔离资源.

Feign Hystrix

因为熔断只是作用在服务调用这一端,因此我们根据上一篇的示例代码只需要改动spring-cloud-consumer项目相关代码就可以。因为,Feign中已经依赖了Hystrix所以在maven配置上不用做任何改动。

1、配置文件

application.properties添加这一条:
feign.hystrix.enabled=true

2、创建回调类

创建HelloRemoteHystrix类继承与HelloRemote实现回调的方法
@Component
public class HelloRemoteHystrix implements HelloRemote{

@Override
public String hello(@RequestParam(value = "name") String name) {
return "hello" +name+", this messge send failed ";
}
}

3、添加fallback属性

HelloRemote类添加指定fallback类,在服务熔断的时候返回fallback类中的内容。
@FeignClient(name= "spring-cloud-producer",fallback = HelloRemoteHystrix.class)
public interface HelloRemote {

@RequestMapping(value = "/hello")
public String hello(@RequestParam(value = "name") String name);

}
改动点就这点,很简单吧。

4、测试

那我们就来测试一下看看效果吧。
依次启动spring-cloud-eureka、spring-cloud-producer、spring-cloud-consumer三个项目。
浏览器中输入:http://localhost:9001/hello/neo
返回:hello neo,this is first messge
说明加入熔断相关信息后,不影响正常的访问。接下来我们手动停止spring-cloud-producer项目再次测试:
浏览器中输入:http://localhost:9001/hello/neo
返回:hello neo, this messge send failed
根据返回结果说明熔断成功。
示例代码-github
示例代码-码云
参考:
使用Spring Cloud与Docker实战微服务
微服务框架Spring Cloud介绍 Part5: 在微服务系统中使用Hystrix, Hystrix Dashboard与Turbine

作者:纯洁的微笑
出处:http://www.ityouknow.com/ 
版权归作者所有,转载请注明出处

基础结构

Page是Innodb存储的最基本结构,也是Innodb磁盘管理的最小单位,与数据库相关的所有内容都存储在Page结构里。Page分为几种类型:数据页(B-Tree Node)Undo页(Undo Log Page)系统页(System Page)事务数据页(Transaction System Page)等;每个数据页的大小为16kb,每个Page使用一个32位(一位表示的就是0或1)的int值来表示,正好对应Innodb最大64TB的存储容量(16kb * 2^32=64tib)
一个Page的基本结构如下:

头部数据

每个page都有通用的头和尾,但是中部的内容根据page的类型不同而发生变化,头部的数据如下:
page头部保存了两个指针,分别指向前一个Page和后一个Page,头部还有Page的类型信息和用来唯一标识Page的编号。根据这个指针分布可以想象到Page链接起来就是一个双向链表

主体数据

在Page的主体部分,主要关注数据和索引的存储,他们都位于User Records部分,User Records占据Page的大部分空间,User Records由一条条的Record组成,每条记录代表索引树上的一个节点(非叶子节点和叶子节点);在一个单链表的内部,单链表的头尾由两条记录来表示,字符串形式的“ Infimum”代表开头,“Supremum”表示结尾;System Record 和 User Record是两个平行的段;
Innodb中存在四种不同的Record,分别是
  1. 主键索引树非叶子节点
  2. 主键索引树叶子节点
  3. 辅助键索引树非叶子节点
  4. 辅助键索引树叶子节点
这四种节点Record格式上有差异,但是内部都存储着Next指针指向下一个Record

User Record

User Record在Page内以单链表的形式存在,最初数据是按照插入的先后顺序排列的,但是随着新数据的插入和旧数据的删除,数据物理顺序发生改变,但是他们依然保持着逻辑上的先后顺序
把User Record组织形式和若干Page组织起来,就得到了稍微完整的形式:

如何定位一个Record:

  1. 通过根节点开始遍历一个索引的B+树,通过各层非叶子节点达到底层的叶子节点的数据页(Page),这个Page内部存放的都是叶子节点
  2. 在Page内部从“Infimum”节点开始遍历单链表(遍历一般会被优化),如果找到键则返回。如果遍历到了“Supremum”,说明当前Page里没有合适的键,这时借助Page页内部的next page指针,跳转到下一个page继续从“Infmum”开始逐个查找

User Record内部的数据

User Record内部存储了四种格式的数据:
  1. 主索引树非叶子节点(绿色)
    • 子节点存储的主键里最小的值,这时B+树必须的,作用是在一个Page里定位到具体的记录的位置
    • 最小的值所在的Page的编号,作用是定位到对应的Record所在的Page
  2. 主索引树叶子节点(黄色)
    • 主键,B+树所必须的,也是数据行的一部分
    • 除去主键以外的所有列,这时数据行的除去主键的其他所有列的集合
  3. 辅助索引树非叶子节点(蓝色)
    • 子节点里存储的辅助键值里的最小值,这时B+Tree必须的,作用是在一个Page里定位到具体记录的位置
  4. 辅助索引树叶子节点(红色)
    • 辅助索引键值,是B+树必须的
    • 主键值,用来在主索引树里在做一次B+树检索来找到整条记录

整体的查找过程

简介的树形查找示意图

Page和B+树之间并没有一一对应的关系,Page只是作为一个Record的保存容器,它存在的目的是便于对磁盘空间进行批量管理

InnoDB是事务安全的存储引擎,设计上借鉴了很多Oracle的架构思想,一般而言,在OLTP应用中,InnoDB应该作为核心应用表的首先存储引擎。InnoDB是由第三方的Innobase Oy公司开发,现已被Oracle收购,创始人是Heikki Tuuri,芬兰赫尔辛基人,和著名的Linux创始人Linus是校友。

InnoDB体系架构

上面是InnoDB的一个简图,简单来说,InnoDB是由一系列后台线程和一大块内存组成。

后台线程

默认情况下,InnoDB的后台线程有7个 —— 4个IO thread, 1个master thread, 1个lock monitor thread, 一个error monitor thread

内存

InnoDB的内存主要有以下几个部分组成:缓冲池 (buffer pool)、重做日志缓冲池(redo log buffer)以及额外的内存池(additional memory pool),如下图所示:



其中缓冲池占最大块内存,用来缓存各自数据,数据文件按页(每页16K)读取到缓冲池,按最近最少使用算法(LRU)保留缓存数据。
缓冲池缓冲的数据类型有:数据页、索引页、插入缓冲、自适应哈希索引、锁信息、数据字典信息等,其中数据页和索引页占了绝大部分内存。

日志缓冲将重做日志信息先放入这个缓冲区,然后按一定频率(默认为1s)将其刷新至重做日志文件。

Master 后台线程

InnoDB的主要工作都是在一个单独的Master线程里完成的。Master线程的优先级最高,它主要分为以下几个循环:主循环(loop)、后台循环(background loop)、刷新循环(flush loop)、暂停循环(suspend loop)。

先来看看主循环,下面是它的伪代码:
  1. void master_thread() (  
  2. loop:  
  3. for (int i =0; i <10; i++){  
  4.     do thing once per second  
  5.     sleep 1 second if necessary  
  6. }  
  7. do things once per ten seconds  
  8. goto loop;  
  9. }  

其中每秒一次的操作包括:
刷新日志缓冲区(总是)
合并插入缓冲(可能)
至多刷新100个脏数据页(可能)
如果没有当前用户活动,切换至background loop (可能)
和Oracle类似,即使事务未提交,也会每秒刷新重做日志缓冲区。

其中每10秒一次的操作包括:
合并至多5个插入缓冲(总是)
刷新日志缓冲(总是)
刷新100个或10个脏页到磁盘(总是)
产生一个检查点(总是)
删除无用Undo 页 (总是)

接着来看后台循环,若当前没有用户活动或数据库关闭时,会切换至该循环执行以下操作:
删除无用的undo页(总是)
合并20个插入缓冲(总是)
跳回到主循环(总是)
不断刷新100个页,直到符合条件跳转到flush loop(可能)
如果flush loop中也没有什么事情可做,边切换到suspend loop,将master线程挂起。


阅读目录(Content)
    在数据库系统中,既有存放数据的文件,也有存放日志的文件。日志在内存中也是有缓存Log buffer,也有磁盘文件log file,本文主要描述存放日志的文件。
    MySQL中的日志文件,有这么两类常常讨论到:undo日志与redo日志。
回到顶部(go to top)

1 undo

1.1 undo是啥

undo日志用于存放数据修改被修改前的值,假设修改 tba 表中 id=2的行数据,把Name='B' 修改为Name = 'B2' ,那么undo日志就会用来存放Name='B'的记录,如果这个修改出现异常,可以使用undo日志来实现回滚操作,保证事务的一致性。
对数据的变更操作,主要来自 INSERT UPDATE DELETE,而UNDO LOG中分为两种类型,一种是 INSERT_UNDO(INSERT操作),记录插入的唯一键值;一种是 UPDATE_UNDO(包含UPDATE及DELETE操作),记录修改的唯一键值以及old column记录。
Id
Name
1
A
2
B
3
C
4
D

1.2 undo参数

MySQL跟undo有关的参数设置有这些:
1 mysql> show global variables like '%undo%';
2 +--------------------------+------------+ 3 | Variable_name | Value | 4 +--------------------------+------------+ 5 | innodb_max_undo_log_size | 1073741824 | 6 | innodb_undo_directory | ./ | 7 | innodb_undo_log_truncate | OFF | 8 | innodb_undo_logs | 128 | 9 | innodb_undo_tablespaces | 3 |10 +--------------------------+------------+11
12 mysql> show global variables like '%truncate%';
13 +--------------------------------------+-------+14 | Variable_name | Value |15 +--------------------------------------+-------+16 | innodb_purge_rseg_truncate_frequency | 128 |17 | innodb_undo_log_truncate | OFF |18 +--------------------------------------+-------+
  • innodb_max_undo_log_size
    控制最大undo tablespace文件的大小,当启动了innodb_undo_log_truncate 时,undo tablespace 超过innodb_max_undo_log_size 阀值时才会去尝试truncate。该值默认大小为1G,truncate后的大小默认为10M。
  • innodb_undo_tablespaces 
    设置undo独立表空间个数,范围为0-128, 默认为0,0表示表示不开启独立undo表空间 且 undo日志存储在ibdata文件中。该参数只能在最开始初始化MySQL实例的时候指定,如果实例已创建,这个参数是不能变动的,如果在数据库配置文 件 .cnf 中指定innodb_undo_tablespaces 的个数大于实例创建时的指定个数,则会启动失败,提示该参数设置有误。
    如果设置了该参数为n(n>0),那么就会在undo目录下创建n个undo文件(undo001,undo002 ...... undo n),每个文件默认大小为10M.
什么时候需要来设置这个参数呢?
    当DB写压力较大时,可以设置独立UNDO表空间,把UNDO LOG从ibdata文件中分离开来,指定 innodb_undo_directory目录存放,可以制定到高速磁盘上,加快UNDO LOG 的读写性能。
  • innodb_undo_log_truncate
   InnoDB的purge线程,根据innodb_undo_log_truncate设置开启或关闭、innodb_max_undo_log_size的参数值,以及truncate的频率来进行空间回收和 undo file 的重新初始化。
   该参数生效的前提是,已设置独立表空间且独立表空间个数大于等于2个。
   purge线程在truncate undo log file的过程中,需要检查该文件上是否还有活动事务,如果没有,需要把该undo log file标记为不可分配,这个时候,undo log 都会记录到其他文件上,所以至少需要2个独立表空间文件,才能进行truncate 操作,标注不可分配后,会创建一个独立的文件undo_<space_id>_trunc.log,记录现在正在truncate 某个undo log文件,然后开始初始化undo log file到10M,操作结束后,删除表示truncate动作的 undo_<space_id>_trunc.log 文件,这个文件保证了即使在truncate过程中发生了故障重启数据库服务,重启后,服务发现这个文件,也会继续完成truncate操作,删除文件结束后,标识该undo log file可分配。
  • innodb_purge_rseg_truncate_frequency
  用于控制purge回滚段的频度,默认为128。假设设置为n,则说明,当Innodb Purge操作的协调线程 purge事务128次时,就会触发一次History purge,检查当前的undo log 表空间状态是否会触发truncate。

1.3 undo空间管理

如果需要设置独立表空间,需要在初始化数据库实例的时候,指定独立表空间的数量。
UNDO内部由多个回滚段组成,即 Rollback segment,一共有128个,保存在ibdata系统表空间中,分别从resg slot0 - resg slot127,每一个resg slot,也就是每一个回滚段,内部由1024个undo segment 组成。
回滚段(rollback segment)分配如下:
回滚段中除去32个提供给临时表事务使用,剩下的 128-32=96个回滚段,可执行 96*1024 个并发事务操作,每个事务占用一个 undo segment slot,注意,如果事务中有临时表事务,还会在临时表空间中的 undo segment slot 再占用一个 undo segment slot,即占用2个undo segment slot。如果错误日志中有:Cannot find a free slot for an undo log。则说明并发的事务太多了,需要考虑下是否要分流业务。
回滚段(rollback segment )采用 轮询调度的方式来分配使用,如果设置了独立表空间,那么就不会使用系统表空间回滚段中undo segment,而是使用独立表空间的,同时,如果回顾段正在 Truncate操作,则不分配。
回到顶部(go to top)

2 redo

2.1 redo是啥

    当数据库对数据做修改的时候,需要把数据页从磁盘读到buffer pool中,然后在buffer pool中进行修改,那么这个时候buffer pool中的数据页就与磁盘上的数据页内容不一致,称buffer pool的数据页为dirty page 脏数据,如果这个时候发生非正常的DB服务重启,那么这些数据还没在内存,并没有同步到磁盘文件中(注意,同步到磁盘文件是个随机IO),也就是会发生数据丢失,如果这个时候,能够在有一个文件,当buffer pool 中的data page变更结束后,把相应修改记录记录到这个文件(注意,记录日志是顺序IO),那么当DB服务发生crash的情况,恢复DB的时候,也可以根据这个文件的记录内容,重新应用到磁盘文件,数据保持一致。
    这个文件就是redo log ,用于记录 数据修改后的记录,顺序记录。它可以带来这些好处:
    假设修改 tba 表中 id=2的行数据,把Name='B' 修改为Name = 'B2' ,那么redo日志就会用来存放Name='B2'的记录,如果这个修改在flush 到磁盘文件时出现异常,可以使用redo log实现重做操作,保证事务的持久性。
Id
Name
1
A
2
B
3
C
4
D
 
    这里注意下redo log 跟binary log 的区别,redo log 是存储引擎层产生的,而binary log是数据库层产生的。假设一个大事务,对tba做10万行的记录插入,在这个过程中,一直不断的往redo log顺序记录,而binary log不会记录,知道这个事务提交,才会一次写入到binary log文件中。binary log的记录格式有3种:row,statement跟mixed,不同格式记录形式不一样。

2.2 redo 参数

  • innodb_log_files_in_group
redo log 文件的个数,命名方式如:ib_logfile0,iblogfile1... iblogfilen。默认2个,最大100个。
  • innodb_log_file_size
文件设置大小,默认值为 48M,最大值为512G,注意最大值指的是整个 redo log系列文件之和,即(innodb_log_files_in_group * innodb_log_file_size )不能大于最大值512G。
  • innodb_log_group_home_dir
文件存放路径
  • innodb_log_buffer_size
Redo Log 缓存区,默认8M,可设置1-8M。延迟事务日志写入磁盘,把redo log 放到该缓冲区,然后根据 innodb_flush_log_at_trx_commit参数的设置,再把日志从buffer 中flush 到磁盘中。
  • innodb_flush_log_at_trx_commit
  • 注意:由于进程调度策略问题,这个“每秒执行一次 flush(刷到磁盘)操作”并不是保证100%的“每秒”。
 

2.3 redo 空间管理

    Redo log文件以ib_logfile[number]命名,Redo log 以顺序的方式写入文件文件,写满时则回溯到第一个文件,进行覆盖写。(但在做redo checkpoint时,也会更新第一个日志文件的头部checkpoint标记,所以严格来讲也不算顺序写)。
实际上redo log有两部分组成:redo log buffer 跟redo log file。buffer pool中把数据修改情况记录到redo log buffer,出现以下情况,再把redo log刷下到redo log file:
回到顶部(go to top)

3 undo及redo如何记录事务

这部分内容推荐阅读这系列的博客,写的好好http://www.zhdba.com/mysqlops/2012/04/06/innodb-log1/
以下内容部分节选自这博客,感谢作者总结,深入浅出超好理解。

3.1 Undo + Redo事务的简化过程

  假设有A、B两个数据,值分别为1,2,开始一个事务,事务的操作内容为:把1修改为3,2修改为4,那么实际的记录如下(简化):
  A.事务开始.
  B.记录A=1到undo log.
  C.修改A=3.
  D.记录A=3到redo log.
  E.记录B=2到undo log.
  F.修改B=4.
  G.记录B=4到redo log.
  H.将redo log写入磁盘。
  I.事务提交

3.2  IO影响

  Undo + Redo的设计主要考虑的是提升IO性能,增大数据库吞吐量。可以看出,B D E G H,均是新增操作,但是B D E G 是缓冲到buffer区,只有G是增加了IO操作,为了保证Redo Log能够有比较好的IO性能,InnoDB 的 Redo Log的设计有以下几个特点:
 
  A. 尽量保持Redo Log存储在一段连续的空间上。因此在系统第一次启动时就会将日志文件的空间完全分配。 以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。
  B. 批量写入日志。日志并不是直接写入文件,而是先写入redo log buffer.当需要将日志刷新到磁盘时 (如事务提交),将许多日志一起写入磁盘.
  C. 并发的事务共享Redo Log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起,
     以减少日志占用的空间。例如,Redo Log中的记录内容可能是这样的:
     记录1: <trx1, insert …>
     记录2: <trx2, update …>
     记录3: <trx1, delete …>
     记录4: <trx3, update …>
     记录5: <trx2, insert …>
  D. 因为C的原因,当一个事务将Redo Log写入磁盘时,也会将其他未提交的事务的日志写入磁盘。
  E. Redo Log上只进行顺序追加的操作,当一个事务需要回滚时,它的Redo Log记录也不会从Redo Log中删除掉。

3.3 恢复

前面说到未提交的事务和回滚了的事务也会记录Redo Log,因此在进行恢复时,这些事务要进行特殊的的处理。有2种不同的恢复策略:
 
  A. 进行恢复时,只重做已经提交了的事务。
  B. 进行恢复时,重做所有事务包括未提交的事务和回滚了的事务。然后通过Undo Log回滚那些
     未提交的事务。
 
  MySQL数据库InnoDB存储引擎使用了B策略, InnoDB存储引擎中的恢复机制有几个特点:
 
  A. 在重做Redo Log时,并不关心事务性。 恢复时,没有BEGIN,也没有COMMIT,ROLLBACK的行为。也不关心每个日志是哪个事务的。尽管事务ID等事务相关的内容会记入Redo Log,这些内容只是被当作要操作的数据的一部分。
  B. 使用B策略就必须要将Undo Log持久化,而且必须要在写Redo Log之前将对应的Undo Log写入磁盘。Undo和Redo Log的这种关联,使得持久化变得复杂起来。为了降低复杂度,InnoDB将Undo Log看作数据,因此记录Undo Log的操作也会记录到redo log中。这样undo log就可以象数据一样缓存起来,而不用在redo log之前写入磁盘了。
     包含Undo Log操作的Redo Log,看起来是这样的:
     记录1: <trx1, Undo log insert <undo_insert …>>
     记录2: <trx1, insert …>
     记录3: <trx2, Undo log insert <undo_update …>>
     记录4: <trx2, update …>
     记录5: <trx3, Undo log insert <undo_delete …>>
     记录6: <trx3, delete …>
  C. 到这里,还有一个问题没有弄清楚。既然Redo没有事务性,那岂不是会重新执行被回滚了的事务?
     确实是这样。同时Innodb也会将事务回滚时的操作也记录到redo log中。回滚操作本质上也是
     对数据进行修改,因此回滚时对数据的操作也会记录到Redo Log中。
     一个回滚了的事务的Redo Log,看起来是这样的:
     记录1: <trx1, Undo log insert <undo_insert …>>
     记录2: <trx1, insert A…>
     记录3: <trx1, Undo log insert <undo_update …>>
     记录4: <trx1, update B…>
     记录5: <trx1, Undo log insert <undo_delete …>>
     记录6: <trx1, delete C…>
     记录7: <trx1, insert C>
     记录8: <trx1, update B to old value>
     记录9: <trx1, delete A>
     一个被回滚了的事务在恢复时的操作就是先redo再undo,因此不会破坏数据的一致性。
 
参考文章:
http://mysql.taobao.org/monthly/2016/07/01/
https://yq.aliyun.com/articles/50747
http://www.zhdba.com/mysqlops/2012/04/06/innodb-log1/
http://mysql.taobao.org/monthly/2015/04/01/
如果转载,请注明博文来源: www.cnblogs.com/xinysu/ ,版权归 博客园 苏家小萝卜 所有。望各位支持!

数据库存放数据的文件,本文称其为data file。
数据库的内容在内存里是有缓存的,这里命名为db buffer。某次操作,我们取了数据库某表格中的数据,这个数据会在内存中缓存一些时间。对这个数据的修改在开始时候也只是修改在内存中的内容。当db buffer已满或者遇到其他的情况,这些数据会写入data file。

undo,redo

日志在内存里也是有缓存的,这里将其叫做log buffer。磁盘上的日志文件称为log file。log file一般是追加内容,可以认为是顺序写,顺序写的磁盘IO开销要小于随机写。
Undo日志记录某数据被修改前的值,可以用来在事务失败时进行rollback;Redo日志记录某数据块被修改后的值,可以用来恢复未写入data file的已成功事务更新的数据。下面的示例来自于杨传辉《大数据分布式存储系统 原理解析与架构实践》,略作改动。
例如某一事务的事务序号为T1,其对数据X进行修改,设X的原值是5,修改后的值为15,那么Undo日志为<T1, X, 5>,Redo日志为<T1, X, 15>
也有把undo和redo结合起来的做法,叫做Undo/Redo日志,在这个例子中Undo/Redo日志为<T1, X, 5, 15>
当用户生成一个数据库事务时,undo log buffer会记录被修改的数据的原始值,redo会记录被修改的数据的更新后的值。
redo日志应首先持久化在磁盘上,然后事务的操作结果才写入db buffer,(此时,内存中的数据和data file对应的数据不同,我们认为内存中的数据是脏数据),db buffer再选择合适的时机将数据持久化到data file中。这种顺序可以保证在需要故障恢复时恢复最后的修改操作。先持久化日志的策略叫做Write Ahead Log,即预写日志。注意:undo日志也应该首先持久化到磁盘上。
在很多系统中,undo日志并非存到日志文件中,而是存放在数据库内部的一个特殊段中。本文中就把这些存储行为都泛化为undo日志存储到undo log file中。
对于某事务T,在log file的记录中必须开始于事务开始标记(比如“start T”),结束于事务结束标记(比如“end T”、”commit T”)。在系统恢复时,如果在log file中某个事务没有事务结束标记,那么需要对这个事务进行undo操作,如果有事务结束标记,则redo。
在db buffer中的内容写入磁盘数据库文件之前,应当把log buffer的内容写入磁盘日志文件。
有一个问题,redo log buffer和undo log buffer存储的事务数量是多少,是按照什么规则将日志写入log file?如果存储的事务数量都是1个,也就意味着是将日志立即刷入磁盘,那么数据的一致性很好保证。在执行事T时,突然断电,如果未对磁盘上的redo log file发生追加操作,可以把这个事务T看做未成功。如果redo log file被修改,则认为事务是成功了,重启数据库使用redo log恢复数据到db buffer和 data file即可。
如果存储多个的话,其实也挺好解释的。就是db buffer写入data file之前,先把日志写入log file。这种方式可以减少磁盘IO,增加吞吐量。不过,这种方式适用于一致性要求不高的场合。因为如果出现断电等系统故障,log buffer、db buffer中的完成的事务会丢失。以转账为例,如果用户的转账事务在这种情况下丢失了,这意味着在系统恢复后用户需要重新转账。

检查点checkpoint

checkpoint是为了定期将db buffer的内容刷新到data file。当遇到内存不足、db buffer已满等情况时,需要将db buffer中的内容/部分内容(特别是脏数据)转储到data file中。在转储时,会记录checkpoint发生的”时刻“。在故障回复时候,只需要redo/undo最近的一次checkpoint之后的操作。

幂等性问题

在日志文件中的操作记录应该具有幂等性。幂等性,就是说同一个操作执行多次和执行一次,结果是一样的。例如,5*1 = 5*1*1*1,所以对5的乘1操作具有幂等性。日志文件在故障恢复中,可能会回放多次(比如第一次回放到一半时系统断电了,不得不再重新回放),如果操作记录不满足幂等性,会造成数据错误。

资料

What Is Undo?
Redo log
Oracle学习笔记:Redo日志(重做日志)的作用 
数据库日志文件– undo log 、redo log、 undo/redo log 
undo log与redo log原理分析
Oracle redo与undo浅析
RedoLog Checkpoint 和 SCN关系 
mysql dba系统学习(10)innodb引擎的redo log日志的原理
2.4 Checkpoint技术
MySQL Innodb日志机制深入分析
《MySQL技术内幕:InnoDB存储引擎(第2版)》


InnoDB存储引擎有三大特性非常令人激动,它们分别是插入缓冲、两次写和自适应哈希,本篇文章先介绍第一个特性 - 插入缓冲(insert buffer)
在InnoDB的内存中的缓冲区域有单独一块叫做“插入缓冲”的区域,下面我们详细来介绍它。

非聚集索引写性能问题

为了阐述非聚集索引写性能问题,我们先来看一个例子:
mysql>create table t (
           id int auto_increment,
           name varchar(30),
           primary key (id));

我们创建了一个表,表的主键是id,id列式自增长的,即当执行插入操作时,id列会自动增长,页中行记录按id顺序存放,不需要随机读取其它页的数据。因此,在这样的情况下(即聚集索引),插入操作效率很高。
但是,在大部分应用中,很少出现表中只有一个聚集索引的情况,更多情况下,表上会有多个非聚集的secondary index (辅助索引)。比如,对于上一张表t,业务上还需要按非唯一的name字段查找,则表定义改为:
mysql>create table t (
           id int auto_increment,
           name varchar(30),
           primary key (id),
           key (name));

这时,除了主键聚合索引外,还产生了一个name列的辅助索引,对于该非聚集索引来说,叶子节点的插入不再有序,这时就需要离散访问非聚集索引页,插入性能变低。

插入缓冲技术机制

为了解决这个问题,InnoDB设计出了插入缓冲技术,对于非聚集类索引的插入和更新操作,不是每一次都直接插入到索引页中,而是先插入到内存中。
具体做法是:
       如果该索引页在缓冲池中,直接插入;
       否则,先将其放入插入缓冲区中,再以一定的频率和索引页合并,
       这时就可以将同一个索引页中的多个插入合并到一个IO操作中,大大提高写性能。
回忆一下在 InnoDB原理之浅谈InnoDB存储引擎中提到的master thread主循环其中的一项工作就是每秒中合并插入缓冲(可能)。这个设计思路和HBase中的LSM树有相似之处,都是通过先在内存中修改,到达一定量后,再和磁盘中的数据合并,目的都是为了提高写性能,具体可参考《HBase LSM树》,这又再一次说明,学到最后,技术都是相通的。
插入缓冲的启用需要满足一下两个条件:
  • 索引是辅助索引(secondary index,非聚集索引)
  • 索引不是唯一的(非唯一值所以)
如果辅助索引是唯一的,就不能使用该技术,原因很简单,因为如果这样做,整个索引数据被切分为2部分,无法保证唯一性。

插入缓冲带来的问题

任何一项技术在带来好处的同时,必然也带来坏处。插入缓冲主要带来如下两个坏处:
1)可能导致数据库宕机后实例恢复时间变长。如果应用程序执行大量的插入和更新操作,且涉及非唯一的聚集索引,一旦出现宕机,这时就有大量内存中的插入缓冲区数据没有合并至索引页中,导致实例恢复时间会很长。
2)在写密集的情况下,插入缓冲会占用过多的缓冲池内存,默认情况下最大可以占用缓冲池内存的1/2,这在实际应用中会带来一定的问题。


今天我们来介绍InnoDB存储引擎的第二个特性 - 两次写(doublewrite),如果说插入缓冲是为了提高写性能的话,那么两次写是为了提高可靠性,牺牲了一点点写性能。

部分写失效

想象这么一个场景,当数据库正在从内存向磁盘写一个数据页时,数据库宕机,从而导致这个页只写了部分数据,这就是部分写失效,它会导致数据丢失。这时是无法通过重做日志(redo log)恢复的,因为重做日志记录的是对页的物理修改,如果页本身已经损坏,重做日志也无能为力。

两次写机制

从上面分析我们知道,在部分写失效的情况下,我们在应用重做日志之前,需要原始页的一个副本,两次写就是为了解决这个问题,下面是它的原理图:
两次写需要额外添加两个部分:
  • 内存中的两次写缓冲(doublewrite buffer),大小为2MB
  • 磁盘上共享表空间中连续的128页,大小也为2MB

其原理是这样的:
(1)当刷新缓冲池脏页时,并不直接写到数据文件中,而是先拷贝至内存中的两次写缓冲区。
(2)接着从两次写缓冲区分两次写入磁盘共享表空间中,每次写入1MB
(3)待第2步完成后,再将两次写缓冲区写入数据文件

这样就可以解决上文提到的部分写失效的问题,因为在磁盘共享表空间中已有数据页副本拷贝,如果数据库在页写入数据文件的过程中宕机,在实例恢复时,可以从共享表空间中找到该页副本,将其拷贝覆盖原有的数据页,再应用重做日志即可。

其中第2步是额外的性能开销,但由于磁盘共享表空间是连续的,因此开销不是很大。可以通过参数skip_innodb_doublewrite禁用两次写功能,默认是开启的,强烈建议开启该功能。


哈希索引是一种非常快的等值查找方法(注意:必须是等值,哈希索引对非等值查找方法无能为力),它查找的时间复杂度为常量。
InnoDB采用自适用哈希索引技术,它会实时监控表上索引的使用情况,如果认为建立哈希索引可以提高查询效率,则自动在内存中的“自适应哈希索引缓冲区”(详见 InnoDB原理之浅谈InnoDB存储引擎中内存构造)建立哈希索引。
之所以该技术称为“自适应”是因为完全由InnoDB自己决定,不需要DBA人为干预。它是通过缓冲池中的B+树构造而来,且不需要对整个表建立哈希索引,因此它的数据非常快
InnoDB官方文档显示,启用自适应哈希索引后,读和写性能可以提高2倍,对于辅助索引的连接操作,性能可以提高5被,因此默认情况下为开启,我们可以通过参数innodb_adaptive_hash_index来禁用此特性。

回顾

在MySQL的众多存储引擎中,只有InnoDB支持事务,所有这里说的事务隔离级别指的是InnoDB下的事务隔离级别。
读未提交:一个事务可以读取到另一个事务未提交的修改。这会带来脏读、幻读、不可重复读问题。(基本没用)
读已提交:一个事务只能读取另一个事务已经提交的修改。其避免了脏读,但仍然存在不可重复读和幻读问题。
可重复读:同一个事务中多次读取相同的数据返回的结果是一样的。其避免了脏读和不可重复读问题,但幻读依然存在。
串行化:事务串行执行。避免了以上所有问题。
以上是SQL-92标准中定义的四种隔离级别。在MySQL中,默认的隔离级别是REPEATABLE-READ(可重复读),并且解决了幻读问题。简单的来说,mysql的默认隔离级别解决了脏读、幻读、不可重复读问题。
不可重复读重点在于update和delete,而幻读的重点在于insert。
在这里,我们只讨论可重复读。

知识储备

MVCC

译注:
  MVCC的全称是“多版本并发控制”。这项技术使得InnoDB的事务隔离级别下执行一致性读操作有了保证,换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值。这是一个可以用来增强并发性的强大的技术,因为这样的一来的话查询就不用等待另一个事务释放锁。这项技术在数据库领域并不是普遍使用的。一些其它的数据库产品,以及mysql其它的存储引擎并不支持它。
 

说明

网上看到大量的文章讲到MVCC都是说给没一行增加两个隐藏的字段分别表示行的创建时间以及过期时间,它们存储的并不是时间,而是事务版本号。
事实上,这种说法并不准确,严格的来讲,InnoDB会给数据库中的每一行增加三个字段,它们分别是DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID。
但是,为了理解的方便,我们可以这样去理解,索引接下来的讲解中也还是用这两个字段的方式去理解。
 

增删查改

在InnoDB中,给每行增加两个隐藏字段来实现MVCC,一个用来记录数据行的创建时间,另一个用来记录行的过期时间(删除时间)。在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。
于是乎,默认的隔离级别(REPEATABLE READ)下,增删查改变成了这样:
  • SELECT
    • 读取创建版本小于或等于当前事务版本号,并且删除版本为空或大于当前事务版本号的记录。这样可以保证在读取之前记录是存在的。
  • INSERT
    • 将当前事务的版本号保存至行的创建版本号
  • UPDATE
    • 新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号
  • DELETE
    • 将当前事务的版本号保存至行的删除版本号
 

快照读和当前读

快照读:读取的是快照版本,也就是历史版本
当前读:读取的是最新版本
普通的SELECT就是快照读,而UPDATE、DELETE、INSERT、SELECT ...  LOCK IN SHARE MODE、SELECT ... FOR UPDATE是当前读。
 

一致性非锁定读和锁定读

锁定读

  在一个事务中,标准的SELECT语句是不会加锁,但是有两种情况例外。SELECT ... LOCK IN SHARE MODE 和 SELECT ... FOR UPDATE。
  SELECT ... LOCK IN SHARE MODE
  给记录假设共享锁,这样一来的话,其它事务只能读不能修改,直到当前事务提交
  SELECT ... FOR UPDATE
  给索引记录加锁,这种情况下跟UPDATE的加锁情况是一样的

一致性非锁定读

  consistent read (一致性读),InnoDB用多版本来提供查询数据库在某个时间点的快照。如果隔离级别是REPEATABLE READ,那么在同一个事务中的所有一致性读都读的是事务中第一个这样的读读到的快照;如果是READ COMMITTED,那么一个事务中的每一个一致性读都会读到它自己刷新的快照版本。Consistent read(一致性读)是READ COMMITTED和REPEATABLE READ隔离级别下普通SELECT语句默认的模式。一致性读不会给它所访问的表加任何形式的锁,因此其它事务可以同时并发的修改它们。
 

悲观锁和乐观锁

悲观锁,正如它的名字那样,数据库总是认为别人会去修改它所要操作的数据,因此在数据库处理过程中将数据加锁。其实现依靠数据库底层。
乐观锁,如它的名字那样,总是认为别人不会去修改,只有在提交更新的时候去检查数据的状态。通常是给数据增加一个字段来标识数据的版本。
 

有这样三种锁我们需要了解
假设一个索引包含以下几个值:10,11,13,20。那么这个索引的next-key锁将会覆盖以下区间:
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
 
了解了以上概念之后,接下来具体就简单分析下REPEATABLE READ隔离级别是如何实现的

理论分析

之所以说是理论分析,是因为要是实际操作证明的话我也不知道怎么去证明,毕竟作者水平实在有限。
但是,这并不意味着我在此胡说八道,有官方文档为证。
这段话的大致意思是,在默认的隔离级别中,普通的SELECT用的是一致性读不加锁。而对于锁定读、UPDATE和DELETE,则需要加锁,至于加什么锁视情况而定。如果你对一个唯一索引使用了唯一的检索条件,那么只需锁定索引记录即可;如果你没有使用唯一索引作为检索条件,或者用到了索引范围扫描,那么将会使用间隙锁或者next-key锁以此来阻塞其它会话向这个范围内的间隙插入数据。
作者曾经有一个误区,认为按照前面说MVCC下的增删查改的行为就不会出现任何问题,也不会出现不可重复读和幻读。但其实是大错特错。
举个很简单的例子,假设事务A更新表中id=1的记录,而事务B也更新这条记录,并且B先提交,如果按照前面MVVC说的,事务A读取id=1的快照版本,那么它看不到B所提交的修改,此时如果直接更新的话就会覆盖B之前的修改,这就不对了,可能B和A修改的不是一个字段,但是这样一来,B的修改就丢失了,这是不允许的。
所以,在修改的时候一定不是快照读,而是当前读。
而且,前面也讲过只有普通的SELECT才是快照读,其它诸如UPDATE、删除都是当前读。修改的时候加锁这是必然的,同时为了防止幻读的出现还需要加间隙锁。
回想一下
1、利用MVCC实现一致性非锁定读,这就有保证在同一个事务中多次读取相同的数据返回的结果是一样的,解决了不可重复读的问题
2、利用Gap Locks和Next-Key可以阻止其它事务在锁定区间内插入数据,因此解决了幻读问题
综上所述,默认隔离级别的实现依赖于MVCC和锁,再具体一点是一致性读和锁。
 

演示

上面四幅截图对比,可以看到由于id是主键,用id作为检索条件时只锁定那一个索引记录。接下来,看索引范围的例子
这两幅截图,可以看出,由于没有使用唯一索引作为检索条件,导致不光锁定了索引记录,还锁定了索引之间的间隙,应该是是使用了next-key锁。
 
参考 https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html

 数据库使用锁是为了支持更好的并发,提供数据的完整性和一致性。InnoDB是一个支持行锁的存储引擎,锁的类型有:共享锁(S)、排他锁(X)、意向共享(IS)、意向排他(IX)。为了提供更好的并发,InnoDB提供了非锁定读:不需要等待访问行上的锁释放,读取行的一个快照。该方法是通过InnoDB的一个特性:MVCC来实现的。
InnoDB有三种行锁的算法:
1,Record Lock:单个行记录上的锁。
2,Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
3,Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

测试一:默认RR隔离级别
root@localhost : test 10:56:10>create table t(a int,key idx_a(a))engine =innodb;
Query OK, 0 rows affected (0.20 sec)

root@localhost : test 10:56:13>insert into t values(1),(3),(5),(8),(11);
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0

root@localhost : test 10:56:15>select * from t;
+------+| a |+------+| 1 || 3 || 5 || 8 || 11 |+------+5 rows in set (0.00 sec)

section A:

root@localhost : test 10:56:27>start transaction;
Query OK, 0 rows affected (0.00 sec)

root@localhost : test 10:56:29>select * from t where a = 8 for update;
+------+| a |+------+| 8 |+------+1 row in set (0.00 sec)


section B:
root@localhost : test 10:54:50>begin;
Query OK, 0 rows affected (0.00 sec)

root@localhost : test 10:56:51>select * from t;
+------+| a |+------+| 1 || 3 || 5 || 8 || 11 |+------+5 rows in set (0.00 sec)

root@localhost : test 10:56:54>insert into t values(2);
Query OK, 1 row affected (0.00 sec)

root@localhost : test 10:57:01>insert into t values(4);
Query OK, 1 row affected (0.00 sec)

++++++++++
root@localhost : test 10:57:04>insert into t values(6);

root@localhost : test 10:57:11>insert into t values(7);

root@localhost : test 10:57:15>insert into t values(9);

root@localhost : test 10:57:33>insert into t values(10);
++++++++++
上面全被锁住,阻塞住了

root@localhost : test 10:57:39>insert into t values(12);
Query OK, 1 row affected (0.00 sec)
问题:
为什么section B上面的插入语句会出现锁等待的情况?InnoDB是行锁,在section A里面锁住了a=8的行,其他应该不受影响。why?
分析:
因为InnoDB对于行的查询都是采用了Next-Key Lock的算法,锁定的不是单个值,而是一个范围(GAP)。上面索引值有1,3,5,8,11,其记录的GAP的区间如下:是一个左开右闭的空间(原因是默认主键的有序自增的特性,结合后面的例子说明)
(-∞,1],(1,3],(3,5],(5,8],(8,11],(11,+∞)
特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock。如上面分析,那就可以解释了。
root@localhost : test 10:56:29>select * from t where a = 8 for update;
+------+| a |+------+| 8 |+------+1 row in set (0.00 sec)
该SQL语句锁定的范围是(5,8],下个下个键值范围是(8,11],所以插入5~11之间的值的时候都会被锁定,要求等待。即:插入5,6,7,8,9,10 会被锁住。插入非这个范围内的值都正常。
################################### 2016-07-21 更新 
因为例子里没有主键,所以要用隐藏的ROWID来代替,数据根据Rowid进行排序。而Rowid是有一定顺序的(自增),所以其中11可以被写入,5不能被写入,不清楚的可以再看一个有主键的例子:
会话1:
01:43:07>create table t(id int,name varchar(10),key idx_id(id),primary key(name))engine =innodb;
Query OK, 0 rows affected (0.02 sec)

01:43:11>insert into t values(1,'a'),(3,'c'),(5,'e'),(8,'g'),(11,'j');
Query OK, 5 rows affected (0.01 sec)
Records: 5 Duplicates: 0 Warnings: 0

01:44:03>select @@global.tx_isolation, @@tx_isolation; +-----------------------+-----------------+| @@global.tx_isolation | @@tx_isolation |+-----------------------+-----------------+| REPEATABLE-READ | REPEATABLE-READ |+-----------------------+-----------------+1 row in set (0.01 sec)

01:44:58>select * from t;
+------+------+| id | name |+------+------+| 1 | a || 3 | c || 5 | e || 8 | g || 11 | j |+------+------+5 rows in set (0.00 sec)

01:45:07>start transaction;

01:45:09>delete from t where id=8;
Query OK, 1 row affected (0.01 sec)


会话2:
01:50:38>select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+| @@global.tx_isolation | @@tx_isolation |+-----------------------+-----------------+| REPEATABLE-READ | REPEATABLE-READ |+-----------------------+-----------------+1 row in set (0.01 sec)

01:50:48>start transaction;

01:50:51>select * from t;
+------+------+| id | name |+------+------+| 1 | a || 3 | c || 5 | e || 8 | g || 11 | j |+------+------+5 rows in set (0.01 sec)

01:51:35>insert into t(id,name) values(6,'f');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted

01:53:32>insert into t(id,name) values(5,'e1');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted

01:53:41>insert into t(id,name) values(7,'h');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted

01:54:43>insert into t(id,name) values(8,'gg');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted

01:55:10>insert into t(id,name) values(9,'k');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted

01:55:23>insert into t(id,name) values(10,'p');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted

01:55:33>insert into t(id,name) values(11,'iz');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted

#########上面看到 id:5678910,11都被锁了。

#########下面看到 id:511 还是可以插入的
01:54:33>insert into t(id,name) values(5,'cz');
Query OK, 1 row affected (0.01 sec)

01:55:59>insert into t(id,name) values(11,'ja');
Query OK, 1 row affected (0.01 sec)
分析:因为会话1已经对id=8的记录加了一个X锁,由于是RR隔离级别,INNODB要防止幻读需要加GAP锁:即id=5(8的左边),id=11(8的右边)之间需要加间隙锁(GAP)。这样[5,e]和[8,g],[8,g]和[11,j]之间的数据都要被锁。上面测试已经验证了这一点,根据索引的有序性,数据按照主键(name)排序,后面写入的[5,cz]([5,e]的左边)和[11,ja]([11,j]的右边)不属于上面的范围从而可以写入。
另外一种情况,把name主键去掉会是怎么样的情况?有兴趣的同学可以测试一下。
##################################################
继续:插入超时失败后,会怎么样?
超时时间的参数:innodb_lock_wait_timeout ,默认是50秒。
超时是否回滚参数:innodb_rollback_on_timeout 默认是OFF。
section A:

root@localhost : test 04:48:51>start transaction;
Query OK, 0 rows affected (0.00 sec)

root@localhost : test 04:48:53>select * from t where a = 8 for update;
+------+| a |+------+| 8 |+------+1 row in set (0.01 sec)


section B:

root@localhost : test 04:49:04>start transaction;
Query OK, 0 rows affected (0.00 sec)

root@localhost : test 04:49:07>insert into t values(12);
Query OK, 1 row affected (0.00 sec)

root@localhost : test 04:49:13>insert into t values(10);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
root@localhost : test 04:50:06>select * from t;
+------+| a |+------+| 1 || 3 || 5 || 8 || 11 || 12 |+------+6 rows in set (0.00 sec)
经过测试,不会回滚超时引发的异常,当参数innodb_rollback_on_timeout 设置成ON时,则可以回滚,会把插进去的12回滚掉。
默认情况下,InnoDB存储引擎不会回滚超时引发的异常,除死锁外。
既然InnoDB有三种算法,那Record Lock什么时候用?还是用上面的列子,把辅助索引改成唯一属性的索引。
测试二:
root@localhost : test 04:58:49>create table t(a int primary key)engine =innodb;
Query OK, 0 rows affected (0.19 sec)

root@localhost : test 04:59:02>insert into t values(1),(3),(5),(8),(11);
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0

root@localhost : test 04:59:10>select * from t;
+----+| a |+----+| 1 || 3 || 5 || 8 || 11 |+----+5 rows in set (0.00 sec)

section A:

root@localhost : test 04:59:30>start transaction;
Query OK, 0 rows affected (0.00 sec)

root@localhost : test 04:59:33>select * from t where a = 8 for update;
+---+| a |+---+| 8 |+---+1 row in set (0.00 sec)

section B:

root@localhost : test 04:58:41>start transaction;
Query OK, 0 rows affected (0.00 sec)

root@localhost : test 04:59:45>insert into t values(6);
Query OK, 1 row affected (0.00 sec)

root@localhost : test 05:00:05>insert into t values(7);
Query OK, 1 row affected (0.00 sec)

root@localhost : test 05:00:08>insert into t values(9);
Query OK, 1 row affected (0.00 sec)

root@localhost : test 05:00:10>insert into t values(10);
Query OK, 1 row affected (0.00 sec)
问题:
为什么section B上面的插入语句可以正常,和测试一不一样?
分析:
因为InnoDB对于行的查询都是采用了Next-Key Lock的算法,锁定的不是单个值,而是一个范围,按照这个方法是会和第一次测试结果一样。但是,当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。
注意:通过主键或则唯一索引来锁定不存在的值,也会产生GAP锁定。即: 
会话1:
04:22:38>show create table t\G
*************************** 1. row ***************************
Table: t
Create Table: CREATE TABLE `t` (
`id` int(11) NOT NULL,
`name` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

04:22:49>start transaction;

04:23:16>select * from t where id = 15 for update;
Empty set (0.00 sec)

会话2:
04:26:10>insert into t(id,name) values(10,'k');
Query OK, 1 row affected (0.01 sec)

04:26:26>insert into t(id,name) values(12,'k');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted
04:29:32>insert into t(id,name) values(16,'kxx');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted
04:29:39>insert into t(id,name) values(160,'kxx');
^CCtrl-C -- sending "KILL QUERY 9851" to server ...
Ctrl-C -- query aborted.
ERROR 1317 (70100): Query execution was interrupted
如何让测试一不阻塞?可以显式的关闭Gap Lock
1:把事务隔离级别改成:Read Committed,提交读、不可重复读。SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
2:修改参数:innodb_locks_unsafe_for_binlog 设置为1。
 
总结:
本文只对 Next-Key Lock 做了一些说明测试,关于锁还有很多其他方面的知识,可以查阅相关资料进行学习。
写完之后的几天刚好牛人写了一篇详细的文章:http://hedengcheng.com/?p=771

Kafka是由LinkedIn开发的一个分布式的消息系统,使用Scala编写,它以可水平扩展和高吞吐率而被广泛使用。目前越来越多的开源分布式处理系统如Cloudera、Apache Storm、Spark都支持与Kafka集成。InfoQ一直在紧密关注Kafka的应用以及发展,“Kafka剖析”专栏将会从架构设计、实现、应用场景、性能等方面深度解析Kafka。

背景介绍

Kafka创建背景

Kafka是一个消息系统,原本开发自LinkedIn,用作LinkedIn的活动流(Activity Stream)和运营数据处理管道(Pipeline)的基础。现在它已被多家不同类型的公司 作为多种类型的数据管道和消息系统使用。
活动流数据是几乎所有站点在对其网站使用情况做报表时都要用到的数据中最常规的部分。活动数据包括页面访问量(Page View)、被查看内容方面的信息以及搜索情况等内容。这种数据通常的处理方式是先把各种活动以日志的形式写入某种文件,然后周期性地对这些文件进行统计分析。运营数据指的是服务器的性能数据(CPU、IO使用率、请求时间、服务日志等等数据)。运营数据的统计方法种类繁多。
相关厂商内容
相关赞助商
近年来,活动和运营数据处理已经成为了网站软件产品特性中一个至关重要的组成部分,这就需要一套稍微更加复杂的基础设施对其提供支持。

Kafka简介

Kafka是一种分布式的,基于发布/订阅的消息系统。主要设计目标如下:

为何使用消息系统

常用Message Queue对比

Kafka架构

Terminology

Kafka拓扑结构

如上图所示,一个典型的Kafka集群中包含若干Producer(可以是web前端产生的Page View,或者是服务器日志,系统CPU、Memory等),若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer Group,以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。

Topic & Partition

Topic在逻辑上可以被认为是一个queue,每条消费都必须指定它的Topic,可以简单理解为必须指明把这条消息放进哪个queue里。为了使得Kafka的吞吐率可以线性提高,物理上把Topic分成一个或多个Partition,每个Partition在物理上对应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件。若创建topic1和topic2两个topic,且分别有13个和19个分区,则整个集群上会相应会生成共32个文件夹(本文所用集群共8个节点,此处topic1和topic2 replication-factor均为1),如下图所示。
每个日志文件都是一个log entrie序列,每个log entrie包含一个4字节整型数值(值为N+5),1个字节的"magic value",4个字节的CRC校验码,其后跟N个字节的消息体。每条消息都有一个当前Partition下唯一的64字节的offset,它指明了这条消息的起始位置。磁盘上存储的消息格式如下:
message length : 4 bytes (value: 1+4+n)
"magic" value : 1 byte
crc : 4 bytes
payload : n bytes
这个log entries并非由一个文件构成,而是分成多个segment,每个segment以该segment第一条消息的offset命名并以“.kafka”为后缀。另外会有一个索引文件,它标明了每个segment下包含的log entry的offset范围,如下图所示。
因为每条消息都被append到该Partition中,属于顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证)。
对于传统的message queue而言,一般会删除已经被消费的消息,而Kafka集群会保留所有的消息,无论其被消费与否。当然,因为磁盘限制,不可能永久保留所有数据(实际上也没必要),因此Kafka提供两种策略删除旧数据。一是基于时间,二是基于Partition文件大小。例如可以通过配置$KAFKA_HOME/config/server.properties,让Kafka删除一周前的数据,也可在Partition文件超过1GB时删除旧数据,配置如下所示。
  
# The minimum age of a log file to be eligible for deletion
log.retention.hours=168
# The maximum size of a log segment file. When this size is reached a new log segment will be created.
log.segment.bytes=1073741824
# The interval at which log segments are checked to see if they can be deleted according to the retention policies
log.retention.check.interval.ms=300000
# If log.cleaner.enable=true is set the cleaner will be enabled and individual logs can then be marked for log compaction.
log.cleaner.enable=false
这里要注意,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高Kafka性能无关。选择怎样的删除策略只与磁盘以及具体的需求有关。另外,Kafka会为每一个Consumer Group保留一些metadata信息——当前消费的消息的position,也即offset。这个offset由Consumer控制。正常情况下Consumer会在消费完一条消息后递增该offset。当然,Consumer也可将offset设成一个较小的值,重新消费一些消息。因为offet由Consumer控制,所以Kafka broker是无状态的,它不需要标记哪些消息被哪些消费过,也不需要通过broker去保证同一个Consumer Group只有一个Consumer能消费某一条消息,因此也就不需要锁机制,这也为Kafka的高吞吐率提供了有力保障。

Producer消息路由

Producer发送消息到broker时,会根据Paritition机制选择将其存储到哪一个Partition。如果Partition机制设置合理,所有消息可以均匀分布到不同的Partition里,这样就实现了负载均衡。如果一个Topic对应一个文件,那这个文件所在的机器I/O将会成为这个Topic的性能瓶颈,而有了Partition后,不同的消息可以并行写入不同broker的不同Partition里,极大的提高了吞吐率。可以在$KAFKA_HOME/config/server.properties中通过配置项num.partitions来指定新建Topic的默认Partition数量,也可在创建Topic时通过参数指定,同时也可以在Topic创建之后通过Kafka提供的工具修改。
在发送一条消息时,可以指定这条消息的key,Producer根据这个key和Partition机制来判断应该将这条消息发送到哪个Parition。Paritition机制可以通过指定Producer的paritition. class这一参数来指定,该class必须实现kafka.producer.Partitioner接口。本例中如果key可以被解析为整数则将对应的整数与Partition总数取余,该消息会被发送到该数对应的Partition。(每个Parition都会有个序号,序号从0开始)
import kafka.producer.Partitioner;
import kafka.utils.VerifiableProperties;

public class JasonPartitioner<T> implements Partitioner {

public JasonPartitioner(VerifiableProperties verifiableProperties) {}

@Override
public int partition(Object key, int numPartitions) {
try {
int partitionNum = Integer.parseInt((String) key);
return Math.abs(Integer.parseInt((String) key) % numPartitions);
} catch (Exception e) {
return Math.abs(key.hashCode() % numPartitions);
}
}
}
如果将上例中的类作为partition.class,并通过如下代码发送20条消息(key分别为0,1,2,3)至topic3(包含4个Partition)。
public void sendMessage() throws InterruptedException{
  for(int i = 1; i <= 5; i++){
   List messageList = new ArrayList<KeyedMessage<String, String>>();
   for(int j = 0; j < 4; j++){
   messageList.add(new KeyedMessage<String, String>("topic2", j+"", "The " + i + " message for key " + j));
   }
   producer.send(messageList);
}
  producer.close();
}
则key相同的消息会被发送并存储到同一个partition里,而且key的序号正好和Partition序号相同。(Partition序号从0开始,本例中的key也从0开始)。下图所示是通过Java程序调用Consumer后打印出的消息列表。

Consumer Group

(本节所有描述都是基于Consumer hight level API而非low level API)。
使用Consumer high level API时,同一Topic的一条消息只能被同一个Consumer Group内的一个Consumer消费,但多个Consumer Group可同时消费这一消息。
这是Kafka用来实现一个Topic消息的广播(发给所有的Consumer)和单播(发给某一个Consumer)的手段。一个Topic可以对应多个Consumer Group。如果需要实现广播,只要每个Consumer有一个独立的Group就可以了。要实现单播只要所有的Consumer在同一个Group里。用Consumer Group还可以将Consumer进行自由的分组而不需要多次发送消息到不同的Topic。
实际上,Kafka的设计理念之一就是同时提供离线处理和实时处理。根据这一特性,可以使用Storm这种实时流处理系统对消息进行实时在线处理,同时使用Hadoop这种批处理系统进行离线处理,还可以同时将数据实时备份到另一个数据中心,只需要保证这三个操作所使用的Consumer属于不同的Consumer Group即可。下图是Kafka在Linkedin的一种简化部署示意图。
下面这个例子更清晰地展示了Kafka Consumer Group的特性。首先创建一个Topic (名为topic1,包含3个Partition),然后创建一个属于group1的Consumer实例,并创建三个属于group2的Consumer实例,最后通过Producer向topic1发送key分别为1,2,3的消息。结果发现属于group1的Consumer收到了所有的这三条消息,同时group2中的3个Consumer分别收到了key为1,2,3的消息。如下图所示。

Push vs. Pull

作为一个消息系统,Kafka遵循了传统的方式,选择由Producer向broker push消息并由Consumer从broker pull消息。一些logging-centric system,比如Facebook的Scribe和Cloudera的Flume,采用push模式。事实上,push模式和pull模式各有优劣。
push模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成Consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据Consumer的消费能力以适当的速率消费消息。
对于Kafka而言,pull模式更合适。pull模式可简化broker的设计,Consumer可自主控制消费消息的速率,同时Consumer可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。

Kafka delivery guarantee

有这么几种可能的delivery guarantee:
总之,Kafka默认保证At least once,并且允许通过设置Producer异步提交来实现At most once。而Exactly once要求与外部存储系统协作,幸运的是Kafka提供的offset可以非常直接非常容易得使用这种方式。